refactor: extract mqtt_demo/ — move bridge, config, logger, appliance descriptors

This commit is contained in:
Marc Billow
2026-07-04 16:11:10 -05:00
parent 24fc197a9e
commit aa9ec0f0d9
17 changed files with 51 additions and 42 deletions
+8 -5
View File
@@ -3,12 +3,15 @@ FROM python:3.11-slim
WORKDIR /app WORKDIR /app
# Python deps first so layer cache survives code changes # Python deps first so layer cache survives code changes
COPY requirements.txt . COPY mqtt_demo/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Application code # Application code — build context is the repo root (see
COPY main.py . # docker-compose.yml's `context: ..`) since mqtt_demo/ depends on the
COPY samsung_appliance/ ./samsung_appliance/ # protocol/ and ocf/ library packages that live alongside it.
COPY protocol/ ./protocol/
COPY ocf/ ./ocf/
COPY mqtt_demo/ ./mqtt_demo/
# /config holds the ab0b0ac4 client cert + key. Mount from the host so # /config holds the ab0b0ac4 client cert + key. Mount from the host so
# secrets aren't baked into the image. # secrets aren't baked into the image.
@@ -28,4 +31,4 @@ ENV CERT_PATH=/config/ab0b0ac4_fullchain.pem \
# No port — bridge is outbound-only (DTLS UDP to appliance, MQTT to broker). # No port — bridge is outbound-only (DTLS UDP to appliance, MQTT to broker).
CMD ["python", "main.py"] CMD ["python", "-m", "mqtt_demo"]
View File
+4 -4
View File
@@ -23,10 +23,10 @@ import threading
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
from samsung_appliance.appliances import get_descriptor from mqtt_demo.samples import get_descriptor
from samsung_appliance.bridge import PushBridge from mqtt_demo.bridge import PushBridge
from samsung_appliance.config import SharedConfig, load_appliances from mqtt_demo.config import SharedConfig, load_appliances
from samsung_appliance.logger import logger from mqtt_demo.logger import logger
def main(): def main():
@@ -23,15 +23,17 @@ import time
import cbor2 import cbor2
from .appliances.base import ApplianceDescriptor, bridge_diagnostic_discovery
from .config import ApplianceConfig, SharedConfig
from .logger import bridge_logger
from protocol.dtls_session import DtlsCoapSession, fmt_code from protocol.dtls_session import DtlsCoapSession, fmt_code
from ocf.keepalive import KeepaliveTask from ocf.keepalive import KeepaliveTask
from ocf.observe_refresh import ObserveRefreshTask from ocf.observe_refresh import ObserveRefreshTask
from ocf.poll_scheduler import PollScheduler from ocf.poll_scheduler import PollScheduler
from ocf.state_cache import StateCache from ocf.state_cache import StateCache
from .descriptor import ApplianceDescriptor, bridge_diagnostic_discovery
from .config import ApplianceConfig, SharedConfig
from .logger import bridge_logger
DEBUG_BRIDGE = os.environ.get('DEBUG_BRIDGE') == '1' DEBUG_BRIDGE = os.environ.get('DEBUG_BRIDGE') == '1'
+20 -15
View File
@@ -20,8 +20,15 @@
# certs in $APPDATA_DIR are preserved. # certs in $APPDATA_DIR are preserved.
set -e set -e
if [ ! -f .env ]; then # This script lives in mqtt_demo/ but the build context is the repo
echo "Error: .env file not found. Copy .env.example to .env and configure it." # root (mqtt_demo/docker-compose.yml uses `context: ..`, since the
# image needs the protocol/ and ocf/ library packages alongside
# mqtt_demo/). Run everything from the repo root so the tar allowlist
# and remote layout line up with that context.
cd "$(dirname "$0")/.."
if [ ! -f mqtt_demo/.env ]; then
echo "Error: mqtt_demo/.env file not found. Copy mqtt_demo/.env.example to mqtt_demo/.env and configure it."
exit 1 exit 1
fi fi
@@ -29,7 +36,7 @@ fi
# Sourcing would tokenize unquoted spaces in values (e.g. # Sourcing would tokenize unquoted spaces in values (e.g.
# `APPLIANCE_1_NAME=Samsung Dryer`) as shell commands. # `APPLIANCE_1_NAME=Samsung Dryer`) as shell commands.
get_env() { get_env() {
grep -E "^${1}=" .env | head -1 | cut -d= -f2- grep -E "^${1}=" mqtt_demo/.env | head -1 | cut -d= -f2-
} }
SSH_HOST=$(get_env SSH_HOST) SSH_HOST=$(get_env SSH_HOST)
REMOTE_DIR=$(get_env REMOTE_DIR) REMOTE_DIR=$(get_env REMOTE_DIR)
@@ -44,22 +51,20 @@ ssh "${SSH_HOST}" mkdir -p "${REMOTE_DIR}" "${APPDATA_DIR}"
# Source code — explicit allowlist instead of an excludelist. Anything # Source code — explicit allowlist instead of an excludelist. Anything
# else in the repo (research files, certs, logs, the .git dir) stays # else in the repo (research files, certs, logs, the .git dir) stays
# local. # local. protocol/ and ocf/ are the library packages mqtt_demo/ imports
# from; they need to land as REMOTE_DIR's siblings of mqtt_demo/ so the
# compose file's `context: ..` resolves the same way it does locally.
COPYFILE_DISABLE=1 tar cz \ COPYFILE_DISABLE=1 tar cz \
main.py \ protocol/ \
samsung_appliance/ \ ocf/ \
Dockerfile \ mqtt_demo/ \
docker-compose.yml \
requirements.txt \
deploy.sh \
README.md \ README.md \
.env.example \
.gitignore \ .gitignore \
| ssh "${SSH_HOST}" "cd ${REMOTE_DIR} && tar xz && find . -name '._*' -delete" | ssh "${SSH_HOST}" "cd ${REMOTE_DIR} && tar xz && find . -name '._*' -delete"
# Ship .env separately and lock it down on the remote. # Ship .env separately and lock it down on the remote.
scp .env "${SSH_HOST}:${REMOTE_DIR}/.env" scp mqtt_demo/.env "${SSH_HOST}:${REMOTE_DIR}/mqtt_demo/.env"
ssh "${SSH_HOST}" "chmod 600 ${REMOTE_DIR}/.env" ssh "${SSH_HOST}" "chmod 600 ${REMOTE_DIR}/mqtt_demo/.env"
# Verify certs are present on the remote — they have to be uploaded # Verify certs are present on the remote — they have to be uploaded
# once before the first build. # once before the first build.
@@ -74,7 +79,7 @@ if ! ssh "${SSH_HOST}" "test -s ${APPDATA_DIR}/client_fullchain.pem && test -s $
fi fi
echo "Rebuilding container…" echo "Rebuilding container…"
ssh "${SSH_HOST}" "cd ${REMOTE_DIR} && docker compose up -d --build" ssh "${SSH_HOST}" "cd ${REMOTE_DIR}/mqtt_demo && docker compose up -d --build"
echo "Done." echo "Done."
echo "Logs: ssh ${SSH_HOST} 'cd ${REMOTE_DIR} && docker compose logs -f'" echo "Logs: ssh ${SSH_HOST} 'cd ${REMOTE_DIR}/mqtt_demo && docker compose logs -f'"
@@ -27,7 +27,7 @@ from dataclasses import dataclass, field
from typing import Callable, Optional, TYPE_CHECKING from typing import Callable, Optional, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from ..poll_scheduler import PollTier from ocf.poll_scheduler import PollTier
@dataclass @dataclass
@@ -1,6 +1,8 @@
services: services:
smartthings-local: smartthings-local:
build: . build:
context: ..
dockerfile: mqtt_demo/Dockerfile
container_name: smartthings-local container_name: smartthings-local
restart: unless-stopped restart: unless-stopped
@@ -9,7 +9,7 @@ Adding a new appliance class:
main.py imports get_descriptor(name) to look up the descriptor at main.py imports get_descriptor(name) to look up the descriptor at
startup; the bridge itself stays class-agnostic. startup; the bridge itself stays class-agnostic.
""" """
from .base import ApplianceDescriptor from ..descriptor import ApplianceDescriptor
from .dryer import DRYER from .dryer import DRYER
from .oven import OVEN from .oven import OVEN
from .fridge import FRIDGE from .fridge import FRIDGE
@@ -6,14 +6,14 @@ samsung_dryer/{bridge,sensors,discovery}.py modules into one place.
""" """
import time import time
from .base import ( from ..descriptor import (
ApplianceDescriptor, ApplianceDescriptor,
avail_base, avail_base,
avail_with_remote, avail_with_remote,
device_block, device_block,
encode, encode,
) )
from ..poll_scheduler import PollTier from ocf.poll_scheduler import PollTier
# --- OBSERVE paths ----------------------------------------------------- # --- OBSERVE paths -----------------------------------------------------
@@ -24,13 +24,13 @@ Key differences from newer Tizen RT firmware:
""" """
import time import time
from .base import ( from ..descriptor import (
ApplianceDescriptor, ApplianceDescriptor,
avail_base, avail_base,
device_block, device_block,
encode, encode,
) )
from ..poll_scheduler import PollTier from ocf.poll_scheduler import PollTier
MODEL = 'ARTIK051_REF_17K' MODEL = 'ARTIK051_REF_17K'
@@ -23,7 +23,7 @@ Untested writes are gated behind <prefix>/remote_available so HA
disables them in the UI when the oven's Remote Control switch is off.""" disables them in the UI when the oven's Remote Control switch is off."""
import time import time
from .base import ( from ..descriptor import (
ApplianceDescriptor, ApplianceDescriptor,
avail_base, avail_base,
avail_with_cycle, avail_with_cycle,
@@ -31,7 +31,7 @@ from .base import (
device_block, device_block,
encode, encode,
) )
from ..poll_scheduler import PollTier from ocf.poll_scheduler import PollTier
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
+3 -1
View File
@@ -33,7 +33,9 @@ from .coap import (
encode_options, parse_coap, build_coap, block_value, fmt_code, encode_options, parse_coap, build_coap, block_value, fmt_code,
split_dtls as _split_dtls, split_dtls as _split_dtls,
) )
from samsung_appliance.logger import logger import logging
logger = logging.getLogger(__name__)
# Diagnostic logging — when DEBUG_BRIDGE=1 in env, the bridge dumps # Diagnostic logging — when DEBUG_BRIDGE=1 in env, the bridge dumps
-5
View File
@@ -1,5 +0,0 @@
"""Samsung appliance local-API → MQTT bridge with HA discovery.
Multi-device: dryer, oven, etc. The device class is selected at startup
via the DEVICE_CLASS env var (see samsung_appliance.appliances)."""
__version__ = "2.0.0"