Add dormant sml2mqtt power-meter bridge (phase 1 of moving HA off edge) #83
@@ -17,6 +17,8 @@ resources:
|
||||
- mosquitto-service.yaml
|
||||
- mosquitto-iot-service.yaml
|
||||
- mosquitto-deployment.yaml
|
||||
- sml2mqtt-config.yaml
|
||||
- sml2mqtt-deployment.yaml
|
||||
- shelly-manager-pvc.yaml
|
||||
- shelly-manager-network-attachment.yaml
|
||||
- shelly-manager-deployment.yaml
|
||||
|
||||
@@ -235,3 +235,69 @@ spec:
|
||||
memory: 32Mi
|
||||
limits:
|
||||
memory: 64Mi
|
||||
---
|
||||
# Fourth broker user: `sml2mqtt` — the power-meter bridge. Publish-only
|
||||
# under `sml2mqtt/#` (ACL in mosquitto-deployment.yaml). Own credential so
|
||||
# its lifecycle is independent of the others. The sml2mqtt pod reads this
|
||||
# password via its init container and renders it into config.yml.
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: mosquitto-sml2mqtt-credentials-bootstrap
|
||||
namespace: home-assistant
|
||||
spec:
|
||||
backoffLimit: 4
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: mosquitto-bootstrap
|
||||
spec:
|
||||
serviceAccountName: mosquitto-bootstrap
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: bootstrap
|
||||
image: alpine:3.21
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
set -eu
|
||||
apk add --no-cache curl jq >/dev/null
|
||||
|
||||
NS=home-assistant
|
||||
NAME=mosquitto-sml2mqtt-credentials
|
||||
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
|
||||
CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
||||
API=https://kubernetes.default.svc
|
||||
|
||||
if curl -fsSL --cacert "$CA" -H "Authorization: Bearer $TOKEN" \
|
||||
"$API/api/v1/namespaces/$NS/secrets/$NAME" >/dev/null 2>&1; then
|
||||
echo "$NAME already exists; nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PW=$(tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)
|
||||
PW_B64=$(printf '%s' "$PW" | base64 | tr -d '\n')
|
||||
|
||||
body=$(jq -nc \
|
||||
--arg name "$NAME" \
|
||||
--arg ns "$NS" \
|
||||
--arg pw "$PW_B64" \
|
||||
'{apiVersion: "v1",
|
||||
kind: "Secret",
|
||||
metadata: {name: $name, namespace: $ns},
|
||||
type: "Opaque",
|
||||
data: {password: $pw}}')
|
||||
|
||||
curl -fsSL --cacert "$CA" -H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" -X POST \
|
||||
--data "$body" \
|
||||
"$API/api/v1/namespaces/$NS/secrets" >/dev/null
|
||||
|
||||
echo "generated $NAME"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
memory: 64Mi
|
||||
|
||||
@@ -40,6 +40,7 @@ spec:
|
||||
mosquitto_passwd -c -b /mosquitto/auth/passwd homeassistant "$HA_PASSWORD"
|
||||
mosquitto_passwd -b /mosquitto/auth/passwd shellies "$SHELLY_PASSWORD"
|
||||
mosquitto_passwd -b /mosquitto/auth/passwd admin "$ADMIN_PASSWORD"
|
||||
mosquitto_passwd -b /mosquitto/auth/passwd sml2mqtt "$SML2MQTT_PASSWORD"
|
||||
# ACL: HA keeps full access (subscribes to everything); the
|
||||
# shellies user is scoped to `shellies/#` for device state +
|
||||
# `homeassistant/#` so HA MQTT-discovery payloads from Shelly
|
||||
@@ -48,6 +49,9 @@ spec:
|
||||
# `admin` is a workstation/debugging credential — full
|
||||
# readwrite, kept on its own credential so its password can
|
||||
# be rotated independently of the HA session.
|
||||
# `sml2mqtt` is the power-meter bridge — publish-only under
|
||||
# its own `sml2mqtt/#` subtree (HA reads it via the full-access
|
||||
# homeassistant user).
|
||||
cat > /mosquitto/auth/acl <<'EOF'
|
||||
user homeassistant
|
||||
topic readwrite #
|
||||
@@ -58,6 +62,9 @@ spec:
|
||||
|
||||
user admin
|
||||
topic readwrite #
|
||||
|
||||
user sml2mqtt
|
||||
topic readwrite sml2mqtt/#
|
||||
EOF
|
||||
chown 1883:1883 /mosquitto/auth/passwd /mosquitto/auth/acl
|
||||
chmod 0600 /mosquitto/auth/passwd /mosquitto/auth/acl
|
||||
@@ -78,6 +85,11 @@ spec:
|
||||
secretKeyRef:
|
||||
name: mosquitto-admin-credentials
|
||||
key: password
|
||||
- name: SML2MQTT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mosquitto-sml2mqtt-credentials
|
||||
key: password
|
||||
ports:
|
||||
- name: mqtt
|
||||
containerPort: 1883
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
# sml2mqtt v3 config TEMPLATE. The broker password is injected at pod
|
||||
# start by the deployment's init container (sed of __MQTT_PASSWORD__ from
|
||||
# the mosquitto-sml2mqtt-credentials Secret) into an emptyDir, because
|
||||
# sml2mqtt reads only a config file and has no env-var override for the
|
||||
# password. The generated password is [A-Za-z0-9] only, so it is sed-safe.
|
||||
#
|
||||
# `Wh in kWh: false` is deliberate: the legacy edl21 sensors publish Wh,
|
||||
# and the new MQTT sensors reuse the same entity_ids — keeping Wh means
|
||||
# Home Assistant long-term statistics continue with zero unit discontinuity.
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: sml2mqtt-config
|
||||
namespace: home-assistant
|
||||
data:
|
||||
config.yml: |
|
||||
logging:
|
||||
level: INFO
|
||||
file: '' # stdout -> kubectl logs / Loki
|
||||
|
||||
mqtt:
|
||||
connection:
|
||||
identifier: sml2mqtt-meter
|
||||
host: mosquitto.home-assistant.svc.cluster.local
|
||||
port: 1883
|
||||
user: sml2mqtt
|
||||
password: __MQTT_PASSWORD__
|
||||
topic prefix: sml2mqtt
|
||||
defaults:
|
||||
qos: 0
|
||||
retain: false
|
||||
last will:
|
||||
topic: status
|
||||
|
||||
general:
|
||||
Wh in kWh: false # keep raw Wh to match edl21 history (see header)
|
||||
republish after: 120 # re-emit unchanged values every 120 s
|
||||
|
||||
inputs:
|
||||
- type: serial
|
||||
url: /dev/stromzaehler # FTDI FT230X, mounted by the deployment
|
||||
timeout: 3
|
||||
# 9600 8N1 are sml2mqtt defaults for SML/EDL21 — no override needed.
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
# sml2mqtt — reads the German SML/EDL21 power meter off its USB FTDI
|
||||
# serial reader and publishes OBIS values to the in-cluster Mosquitto
|
||||
# broker. This replaces Home Assistant's built-in `edl21` integration so
|
||||
# the heavy HA pod no longer needs the USB device and can leave k8s-edge.
|
||||
#
|
||||
# Pinned to k8s-edge because the meter's USB reader is physically on that
|
||||
# node (a genuine hardware constraint, not a stability pin). The pod is
|
||||
# tiny, so it's an acceptable resident of the soon-to-be-tiny edge box.
|
||||
#
|
||||
# SERIAL EXCLUSIVITY: the meter's /dev/ttyUSB0 is a single serial stream;
|
||||
# edl21 (in the HA pod) and sml2mqtt cannot both read it without
|
||||
# corrupting each other's SML framing. Therefore this Deployment ships at
|
||||
# `replicas: 0` (dormant). The cutover is: disable edl21 in HA -> scale
|
||||
# this to 1 -> add HA MQTT sensors -> evict HA from edge. Until then,
|
||||
# merging this changes nothing at runtime.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: sml2mqtt
|
||||
namespace: home-assistant
|
||||
labels:
|
||||
app.kubernetes.io/name: sml2mqtt
|
||||
spec:
|
||||
# Dormant until the edl21 -> MQTT cutover frees the serial port. See header.
|
||||
replicas: 0
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: sml2mqtt
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: sml2mqtt
|
||||
spec:
|
||||
# USB meter reader is on k8s-edge; tolerate the pinned taint to land there.
|
||||
nodeSelector:
|
||||
topology.kubernetes.io/zone: edge
|
||||
tolerations:
|
||||
- key: workload-class
|
||||
operator: Equal
|
||||
value: pinned
|
||||
effect: NoSchedule
|
||||
enableServiceLinks: false
|
||||
# The injected config.yml (with the broker password) is written here
|
||||
# by the init container and read by the main container at /sml2mqtt.
|
||||
initContainers:
|
||||
- name: render-config
|
||||
image: alpine:3.21
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
set -eu
|
||||
sed "s/__MQTT_PASSWORD__/${MQTT_PASSWORD}/" \
|
||||
/tmpl/config.yml > /sml2mqtt/config.yml
|
||||
env:
|
||||
- name: MQTT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mosquitto-sml2mqtt-credentials
|
||||
key: password
|
||||
volumeMounts:
|
||||
- name: config-tmpl
|
||||
mountPath: /tmpl
|
||||
readOnly: true
|
||||
- name: rendered
|
||||
mountPath: /sml2mqtt
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 16Mi
|
||||
limits:
|
||||
memory: 32Mi
|
||||
containers:
|
||||
- name: sml2mqtt
|
||||
image: spacemanspiff2007/sml2mqtt:3.6.0 # pinned; no image-automation by design (critical meter bridge)
|
||||
volumeMounts:
|
||||
- name: rendered
|
||||
mountPath: /sml2mqtt
|
||||
- name: stromzaehler
|
||||
mountPath: /dev/stromzaehler
|
||||
securityContext:
|
||||
# The meter device is root:root 0600 on the node, so the
|
||||
# process must run as root to open it. No `privileged` needed —
|
||||
# owner-root + the hostPath device mount is sufficient.
|
||||
runAsUser: 0
|
||||
runAsGroup: 0
|
||||
resources:
|
||||
requests:
|
||||
cpu: 25m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
memory: 128Mi
|
||||
volumes:
|
||||
- name: config-tmpl
|
||||
configMap:
|
||||
name: sml2mqtt-config
|
||||
- name: rendered
|
||||
emptyDir: {}
|
||||
- name: stromzaehler
|
||||
hostPath:
|
||||
path: /dev/serial/by-id/usb-FTDI_FT230X_Basic_UART_DK0E1GP5-if00-port0
|
||||
Reference in New Issue
Block a user