feat(zigbee2mqtt): log in to MQTT by client certificate (REN-953)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful

Z2M, discovery-guard and interview-watch use the certificate of the
register account zigbee2mqtt-bridge on the mTLS listener :8884 instead of
the legacy password. Z2M gets server/ca/cert/key by env because its
configuration.yaml on the PVC is never re-seeded. The init container no
longer renders mqtt_password. The legacy broker user stays until a later
broker roll.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
atlas-bot
2026-10-07 00:50:39 +00:00
co-authored by Claude Opus 5.5
parent da3037cb24
commit 5be1d0a250
5 changed files with 91 additions and 69 deletions
@@ -11,11 +11,11 @@ data:
# config once the PVC has been seeded — change the on-PVC file (via the Z2M
# frontend or an edit) instead. Kept here so a fresh PVC comes up configured.
#
# Secrets are NOT in this file: both the MQTT password and the Zigbee
# network_key are quoted parseValueRefs (`"!secret <key>"`) resolved from
# /app/data/secret.yaml, which the init container renders — the MQTT password
# from the broker-credential Secret, and the network_key extracted from the
# OpenBao coordinator backup. The non-secret network params (channel/pan_id/
# Secrets are NOT in this file: the Zigbee network_key is a quoted
# parseValueRef (`"!secret <key>"`) resolved from /app/data/secret.yaml,
# which the init container renders from the OpenBao coordinator backup. MQTT
# uses the client certificate mounted at /mqtt-tls (REN-953); the Deployment
# also sets the mqtt server/ca/cert/key by env, which wins over this file. The non-secret network params (channel/pan_id/
# ext_pan_id) are in `advanced` below.
configuration.yaml: |
# Home Assistant MQTT discovery — Z2M publishes device configs under the
@@ -32,14 +32,12 @@ data:
mqtt:
base_topic: zigbee2mqtt
server: mqtt://mosquitto:1883
user: zigbee2mqtt
# MUST be a QUOTED string: Z2M reads secrets via parseValueRef on the
# string value (regex `!<file> <key>`), NOT a YAML `!secret` tag. Unquoted
# `!secret ...` is parsed as a tag and throws "unknown scalar tag
# !<!secret>". The init container renders /app/data/secret.yaml with
# `mqtt_password` from the broker-credential Secret.
password: "!secret mqtt_password"
# Mutual TLS: the broker takes the username from the certificate CN
# (zigbee2mqtt-bridge), so no user or password here.
server: mqtts://mosquitto:8884
ca: /mqtt-tls/ca.crt
cert: /mqtt-tls/tls.crt
key: /mqtt-tls/tls.key
# SMLIGHT SLZB-06 (Texas Instruments CC2652, zigpy-znp under ZHA) reached
# over TCP on the IoT VLAN — the SAME coordinator ZHA uses today
@@ -32,9 +32,11 @@ spec:
# backup also handles the ZHA<->Z2M ext_pan_id byte-order reversal —
# which is why we restore a backup rather than hand-set advanced.ext_pan_id).
# Renders /app/data/secret.yaml (referenced by the config as quoted
# "!secret" parseValueRefs) with the MQTT password AND the Zigbee
# network_key, the latter extracted from the OpenBao coordinator backup
# as a natural-byte-order int array.
# "!secret" parseValueRefs) with the Zigbee network_key, extracted from
# the OpenBao coordinator backup as a natural-byte-order int array. No
# MQTT password since REN-953: `mqtt_password` is absent, so Z2M's
# "!secret mqtt_password" resolves to nothing and it logs in by
# certificate alone.
- name: seed-data
# Our own alpine + jq image, built by build-ci-images.yml. DATED tag,
# never :latest — rebuilding this base for an unrelated consumer
@@ -114,7 +116,6 @@ spec:
# hex key "fe0b..2d" -> "0xfe,0x0b,..,0x2d" (natural order)
NK=$(printf '%s' "$KEY" | sed 's/../0x&,/g; s/,$//')
cat > /app/data/secret.yaml <<EOF
mqtt_password: "${MQTT_PASSWORD}"
network_key: [${NK}]
EOF
if [ ! -f /app/data/configuration.yaml ]; then
@@ -139,12 +140,6 @@ spec:
mkdir -p /app/data/external_converters
cp /external-converters/*.js /app/data/external_converters/
echo "copied external converters: $(ls /external-converters)"
env:
- name: MQTT_PASSWORD
valueFrom:
secretKeyRef:
name: mosquitto-zigbee2mqtt-credentials
key: password
volumeMounts:
- name: data
mountPath: /app/data
@@ -175,12 +170,29 @@ spec:
value: Europe/Berlin
- name: ZIGBEE2MQTT_DATA
value: /app/data
# MQTT by client certificate (register account zigbee2mqtt-bridge,
# REN-953). Env, not the seed template: configuration.yaml on the
# PVC is Z2M's own and is never re-seeded. Z2M copies env values
# into that file on its next settings write, so a rollback must
# set these to the old values, not just drop them. The file keeps
# `user: zigbee2mqtt`; the broker ignores it and uses the CN.
- name: ZIGBEE2MQTT_CONFIG_MQTT_SERVER
value: mqtts://mosquitto:8884
- name: ZIGBEE2MQTT_CONFIG_MQTT_CA
value: /mqtt-tls/ca.crt
- name: ZIGBEE2MQTT_CONFIG_MQTT_CERT
value: /mqtt-tls/tls.crt
- name: ZIGBEE2MQTT_CONFIG_MQTT_KEY
value: /mqtt-tls/tls.key
ports:
- name: http
containerPort: 8080
volumeMounts:
- name: data
mountPath: /app/data
- name: mqtt-tls
mountPath: /mqtt-tls
readOnly: true
# The frontend only serves once Z2M has started its adapter+MQTT, so
# an HTTP 200 on / is a decent liveness/readiness signal. Generous
# startup budget covers coordinator connect + herdsman network adopt.
@@ -239,19 +251,16 @@ spec:
imagePullPolicy: IfNotPresent
command: [sh, /script/watch.sh]
env:
- name: MQTT_PASSWORD
valueFrom:
secretKeyRef:
name: mosquitto-zigbee2mqtt-credentials
key: password
# These three must track zigbee2mqtt-config.yaml: MQTT_HOST the
# mosquitto Service name, MQTT_USER and BASE_TOPIC the mqtt.user and
# mqtt.base_topic it connects with. Set explicitly rather than left
# to the script's defaults so the coupling is visible here.
# Must track the Z2M container: MQTT_HOST/MQTT_PORT the broker it
# connects to, BASE_TOPIC its mqtt.base_topic. Set explicitly
# rather than left to the script's defaults so the coupling is
# visible here. Logs in with the pod's certificate (/mqtt-tls).
- name: MQTT_HOST
value: mosquitto
- name: MQTT_USER
value: zigbee2mqtt
- name: MQTT_PORT
value: "8884"
- name: MQTT_TLS_DIR
value: /mqtt-tls
- name: BASE_TOPIC
value: zigbee2mqtt
- name: STATEDIR
@@ -274,6 +283,9 @@ spec:
- name: interview-watch-script
mountPath: /script
readOnly: true
- name: mqtt-tls
mountPath: /mqtt-tls
readOnly: true
# Cooldown stamps. An explicit emptyDir rather than the image's
# /tmp, so it stays writable as non-root without depending on the
# container filesystem.
@@ -305,19 +317,16 @@ spec:
imagePullPolicy: IfNotPresent
command: [sh, /script/guard.sh]
env:
# Reuses the Z2M broker credential: its ACL is `readwrite
# zigbee2mqtt/#`, which covers reading bridge/state/bridge/groups and
# writing bridge/request/restart. No new broker user needed.
- name: MQTT_PASSWORD
valueFrom:
secretKeyRef:
name: mosquitto-zigbee2mqtt-credentials
key: password
# Must track zigbee2mqtt-config.yaml, same as interview-watch.
# Shares the Z2M certificate: its ACL is `readwrite zigbee2mqtt/#`,
# which covers reading bridge/state/bridge/groups and writing
# bridge/request/restart. Must track the Z2M container, same as
# interview-watch.
- name: MQTT_HOST
value: mosquitto
- name: MQTT_USER
value: zigbee2mqtt
- name: MQTT_PORT
value: "8884"
- name: MQTT_TLS_DIR
value: /mqtt-tls
- name: BASE_TOPIC
value: zigbee2mqtt
- name: STATEDIR
@@ -333,6 +342,9 @@ spec:
- name: discovery-guard-script
mountPath: /script
readOnly: true
- name: mqtt-tls
mountPath: /mqtt-tls
readOnly: true
# Restart cooldown stamp. Its own emptyDir rather than the image's
# /tmp, so it stays writable as non-root.
- name: discovery-guard-state
@@ -359,6 +371,15 @@ spec:
- name: interview-watch-script
configMap:
name: zigbee2mqtt-interview-watch-script
# Client certificate of the register account zigbee2mqtt-bridge,
# issued by mqtt-account-controller. Not subPath, so a cert-manager
# renewal reaches the files in place; Z2M reads them at connect, the
# sidecars on every call. 0444 because the sidecars run as UID 1883
# and Z2M as root; only this pod mounts it.
- name: mqtt-tls
secret:
secretName: mqtt-zigbee2mqtt-bridge-tls
defaultMode: 0444
- name: interview-watch-state
emptyDir:
medium: Memory
@@ -61,16 +61,19 @@
# herdsman-only restart. discover() republishes only configs that are missing or
# whose payload changed, which after a wipe is all of them.
#
# Credentials: reuses the existing zigbee2mqtt broker user, whose ACL is
# Credentials: the pod's client certificate (register account
# zigbee2mqtt-bridge, REN-953) on the mTLS listener :8884. Its ACL is
# `readwrite zigbee2mqtt/#` — enough to read bridge/state and bridge/groups and
# to write bridge/request/restart. No new broker user, and no read grant on
# homeassistant/# (checking for the discovery configs directly would have needed
# one, which the ACL denies on purpose).
# to write bridge/request/restart. No read grant on homeassistant/# (checking
# for the discovery configs directly would have needed one, which the ACL
# denies on purpose).
set -eu
MQTT_HOST="${MQTT_HOST:-mosquitto}"
MQTT_PORT="${MQTT_PORT:-1883}"
MQTT_USER="${MQTT_USER:-zigbee2mqtt}"
MQTT_PORT="${MQTT_PORT:-8884}"
# ca.crt, tls.crt, tls.key of the client certificate. The broker takes the
# username from the certificate CN.
MQTT_TLS_DIR="${MQTT_TLS_DIR:-/mqtt-tls}"
BASE_TOPIC="${BASE_TOPIC:-zigbee2mqtt}"
# Poll cadence. Worst-case time-to-repair is CHECK_INTERVAL + CONFIRM_DELAY.
CHECK_INTERVAL="${CHECK_INTERVAL:-300}"
@@ -105,7 +108,7 @@ log() { echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] discovery-guard: $*"; }
# ever paid when the topic is genuinely empty.
retained() {
timeout "$((READ_TIMEOUT + 5))" mosquitto_sub -h "$MQTT_HOST" -p "$MQTT_PORT" \
-u "$MQTT_USER" -P "$MQTT_PASSWORD" \
--cafile "$MQTT_TLS_DIR/ca.crt" --cert "$MQTT_TLS_DIR/tls.crt" --key "$MQTT_TLS_DIR/tls.key" \
-t "$1" -C 1 -W "$READ_TIMEOUT" 2>/dev/null || true
}
@@ -152,7 +155,7 @@ while true; do
log "confirmed — requesting a Zigbee2MQTT restart to re-run discovery"
if timeout "$PUB_TIMEOUT" mosquitto_pub -h "$MQTT_HOST" -p "$MQTT_PORT" \
-u "$MQTT_USER" -P "$MQTT_PASSWORD" \
--cafile "$MQTT_TLS_DIR/ca.crt" --cert "$MQTT_TLS_DIR/tls.crt" --key "$MQTT_TLS_DIR/tls.key" \
-t "$BASE_TOPIC/bridge/request/restart" -m '' 2>/dev/null; then
date -u +%s > "$STAMP"
log "restart requested"
@@ -53,7 +53,10 @@ set -u
DATA=/app/data
DB="$DATA/database.db"
MQTT_HOST=${MQTT_HOST:-mosquitto}
MQTT_USER=${MQTT_USER:-zigbee2mqtt}
MQTT_PORT=${MQTT_PORT:-8884}
# ca.crt, tls.crt, tls.key of the pod's client certificate (register account
# zigbee2mqtt-bridge, REN-953). The broker takes the username from its CN.
MQTT_TLS_DIR=${MQTT_TLS_DIR:-/mqtt-tls}
# Must track mqtt.base_topic in zigbee2mqtt-config.yaml. Overridable via env so
# the two can be kept in step from the Deployment rather than by editing this
# script; the MQTT ACL for this user is scoped to `zigbee2mqtt/#`, so a change
@@ -219,10 +222,6 @@ while :; do
fi
log "frame from $ieee (nwk=$nwk cluster=$cluster) while awake -> requesting interview"
# NOTE: mosquitto_pub can only take a password via argv (-P, or an -L URL);
# it has no --pw-file or env option, so this cannot be avoided with this
# client. Not a cross-container exposure: the pod does not set
# shareProcessNamespace, so /proc is not shared with the Z2M container.
# Bounded with `timeout`, which also covers a TCP connect that hangs rather
# than being refused. The read loop is single-threaded, so an unbounded
# publish would stall it and miss other devices' wake windows.
@@ -230,7 +229,8 @@ while :; do
# NOT `mosquitto_pub -W`: that flag does not exist for mosquitto_pub (it is
# a mosquitto_sub option), and passing it makes every publish fail on a
# usage error — verified against the client in this image.
if timeout "$PUB_TIMEOUT" mosquitto_pub -h "$MQTT_HOST" -u "$MQTT_USER" -P "$MQTT_PASSWORD" \
if timeout "$PUB_TIMEOUT" mosquitto_pub -h "$MQTT_HOST" -p "$MQTT_PORT" \
--cafile "$MQTT_TLS_DIR/ca.crt" --cert "$MQTT_TLS_DIR/tls.crt" --key "$MQTT_TLS_DIR/tls.key" \
-t "$BASE_TOPIC/bridge/request/device/interview" \
-m "{\"id\":\"$ieee\"}" 2>/dev/null; then
# Stamp only on a SUCCESSFUL publish. Stamping unconditionally would burn
@@ -103,25 +103,25 @@ POD=$(kubectl get pod -n home-assistant -l app.kubernetes.io/name=zigbee2mqtt -o
```
```sh
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"msTemperatureMeasurement\",\"attribute\":\"measuredValue\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":10}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p "$MQTT_PORT" --cafile "$MQTT_TLS_DIR/ca.crt" --cert "$MQTT_TLS_DIR/tls.crt" --key "$MQTT_TLS_DIR/tls.key" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"msTemperatureMeasurement\",\"attribute\":\"measuredValue\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":10}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"msRelativeHumidity\",\"attribute\":\"measuredValue\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":50}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p "$MQTT_PORT" --cafile "$MQTT_TLS_DIR/ca.crt" --cert "$MQTT_TLS_DIR/tls.crt" --key "$MQTT_TLS_DIR/tls.key" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"msRelativeHumidity\",\"attribute\":\"measuredValue\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":50}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"msPressureMeasurement\",\"attribute\":\"measuredValue\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p "$MQTT_PORT" --cafile "$MQTT_TLS_DIR/ca.crt" --cert "$MQTT_TLS_DIR/tls.crt" --key "$MQTT_TLS_DIR/tls.key" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"msPressureMeasurement\",\"attribute\":\"measuredValue\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"msIlluminanceMeasurement\",\"attribute\":\"measuredValue\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":5}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p "$MQTT_PORT" --cafile "$MQTT_TLS_DIR/ca.crt" --cert "$MQTT_TLS_DIR/tls.crt" --key "$MQTT_TLS_DIR/tls.key" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"msIlluminanceMeasurement\",\"attribute\":\"measuredValue\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":5}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90Wind\",\"attribute\":\"windSpeed\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p "$MQTT_PORT" --cafile "$MQTT_TLS_DIR/ca.crt" --cert "$MQTT_TLS_DIR/tls.crt" --key "$MQTT_TLS_DIR/tls.key" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90Wind\",\"attribute\":\"windSpeed\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90Wind\",\"attribute\":\"windDirection\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p "$MQTT_PORT" --cafile "$MQTT_TLS_DIR/ca.crt" --cert "$MQTT_TLS_DIR/tls.crt" --key "$MQTT_TLS_DIR/tls.key" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90Wind\",\"attribute\":\"windDirection\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90Wind\",\"attribute\":\"gustSpeed\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p "$MQTT_PORT" --cafile "$MQTT_TLS_DIR/ca.crt" --cert "$MQTT_TLS_DIR/tls.crt" --key "$MQTT_TLS_DIR/tls.key" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90Wind\",\"attribute\":\"gustSpeed\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90UV\",\"attribute\":\"uvIndex\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p "$MQTT_PORT" --cafile "$MQTT_TLS_DIR/ca.crt" --cert "$MQTT_TLS_DIR/tls.crt" --key "$MQTT_TLS_DIR/tls.key" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90UV\",\"attribute\":\"uvIndex\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90Rain\",\"attribute\":\"rainStatus\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p "$MQTT_PORT" --cafile "$MQTT_TLS_DIR/ca.crt" --cert "$MQTT_TLS_DIR/tls.crt" --key "$MQTT_TLS_DIR/tls.key" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90Rain\",\"attribute\":\"rainStatus\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p 1883 -u "$MQTT_USER" -P "$MQTT_PASSWORD" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90Rain\",\"attribute\":\"precipitation\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
kubectl exec -n home-assistant "$POD" -c discovery-guard -- sh -c 'mosquitto_pub -h "$MQTT_HOST" -p "$MQTT_PORT" --cafile "$MQTT_TLS_DIR/ca.crt" --cert "$MQTT_TLS_DIR/tls.crt" --key "$MQTT_TLS_DIR/tls.key" -t "$BASE_TOPIC/bridge/request/device/configure_reporting" -m "{\"id\":\"0xc02cedfffe958f40\",\"endpoint\":1,\"cluster\":\"shellyWS90Rain\",\"attribute\":\"precipitation\",\"minimum_report_interval\":5,\"maximum_report_interval\":3600,\"reportable_change\":1,\"options\":{\"manufacturerCode\":5264}}"'
```
These are idempotent — re-sending a request that matches the current