Compare commits

..
10 Commits
Author SHA1 Message Date
azykov@mail.ru d66fef8f2b jitsi fixed passwords 2026-11-04 14:25:37 +03:00
azykov@mail.ru 9865f5b386 dedicated metallb pool 10.0.64.9 for jitsi 2026-11-04 13:59:34 +03:00
azykov@mail.ru 5ccf1ffffa added public ip to jitsui 2026-11-04 13:02:23 +03:00
azykov@mail.ru 6ca5c7b396 jitsi server 2026-11-04 12:59:48 +03:00
azykov@mail.ru 73e25266df generous retries for ntfy and map_monitor 2026-10-26 14:41:05 +03:00
azykov@mail.ru 0050dbc3af map_monitor 2026-10-26 14:36:42 +03:00
azykov@mail.ru 9505761c9b ntfy acl fix 2026-10-26 12:45:02 +03:00
azykov@mail.ru 224fffe7d2 added ha user to ntfy 2026-10-26 11:50:48 +03:00
azykov@mail.ru 815e29ed16 fixed ntfy NTFY_AUTH_ACCESS error 2026-10-26 11:30:44 +03:00
azykov@mail.ru 2ae3c26880 ntfy image name typo fixed 2026-10-26 11:20:10 +03:00
22 changed files with 985 additions and 9 deletions
+7 -1
View File
@@ -43,4 +43,10 @@ creation_rules:
age: age1rvyt7nlnjqeyt6ykwh9fj9d88kjmq9vetcx667ul3ql6fyfz4gqqh9lq9q age: age1rvyt7nlnjqeyt6ykwh9fj9d88kjmq9vetcx667ul3ql6fyfz4gqqh9lq9q
- path_regex: ntfy.*\.enc\.yaml$ - path_regex: ntfy.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$ encrypted_regex: ^(data|stringData)$
age: age1uzs33ep6k3htnuwyyp54y373wkf7urd0qvk5kxcmptr5tt0fffus4v06gn age: age1uzs33ep6k3htnuwyyp54y373wkf7urd0qvk5kxcmptr5tt0fffus4v06gn
- path_regex: map_monitor.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1k4j5vyqqt7w7k52gvkpfe6znqr9mlgt076l7xgdtyc75lamsh4aqshj5qs
- path_regex: jitsi.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1zmr32r5mcgyh73asvzl8l690pzwqsx9hle27u3cydngsgv56wykq3v67px
+77
View File
@@ -0,0 +1,77 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: jitsi
namespace: video
spec:
interval: 30m
chart:
spec:
chart: jitsi-meet
version: ">=1.0.0 <2.0.0"
sourceRef:
kind: HelmRepository
name: jitsi
install:
remediation:
retries: -1
upgrade:
remediation:
retries: 5
driftDetection:
mode: enabled
# fixed xmpp credentials: the chart generates random ones on every render, which
# desyncs jicofo/jvb from the already running prosody after an upgrade
valuesFrom:
- kind: Secret
name: jitsi-secrets
valuesKey: JICOFO_AUTH_PASSWORD
targetPath: jicofo.xmpp.password
- kind: Secret
name: jitsi-secrets
valuesKey: JICOFO_COMPONENT_SECRET
targetPath: jicofo.xmpp.componentSecret
- kind: Secret
name: jitsi-secrets
valuesKey: JVB_AUTH_PASSWORD
targetPath: jvb.xmpp.password
values:
publicURL: https://v.aggtaa.com
web:
ingress:
enabled: true
ingressClassName: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: v.aggtaa.com
paths: ["/"]
tls:
- secretName: jitsi-tls
hosts:
- v.aggtaa.com
# media traffic: UDP 10000 must be forwarded from the WAN router to the JVB service ip (kubectl get svc -n video)
jvb:
UDPPort: 10000
# wan ip first, then the service ip for clients on the lan (no hairpin nat needed);
# the chart passes this string as-is to JVB_ADVERTISE_IPS, which accepts a comma separated list
publicIPs:
- 91.233.45.43,10.0.64.9
service:
enabled: true
type: LoadBalancer
externalTrafficPolicy: Cluster
postRenderers:
- kustomize:
patches:
# dmz-pool has a single ip held by traefik, so use the dedicated public-ports-pool (see infra/metallb/config.yaml)
- target:
kind: Service
name: jitsi-jitsi-meet-jvb
patch: |
- op: add
path: /metadata/annotations/metallb.io~1address-pool
value: public-ports-pool
@@ -0,0 +1,8 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: jitsi
namespace: video
spec:
interval: 1h
url: https://jitsi-contrib.github.io/jitsi-helm/
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- helmrelease.yaml
- secret.enc.yaml
+24
View File
@@ -0,0 +1,24 @@
apiVersion: v1
kind: Secret
metadata:
name: jitsi-secrets
namespace: video
stringData:
JICOFO_AUTH_PASSWORD: ENC[AES256_GCM,data:OUEZWPlqpkuhEI8kf65kKUooeahC3z03STevw7oOWx8=,iv:KSfrwehZG9kW7bfzQ5yavq6gYj+TrYKS/LZPNzaZdLI=,tag:/FiUF75ib3RXPuzO0EhIpw==,type:str]
JICOFO_COMPONENT_SECRET: ENC[AES256_GCM,data:y8a84NdaClBRUFDPtXhulJxrX68Tn+4s1NrNqyhBjvI=,iv:fdqlgRwHNTdqegvfOrvX1Iph5aMKAtyW5zJUr+kBNx0=,tag:yNQ53KQQWafV7CSbNttJUQ==,type:str]
JVB_AUTH_PASSWORD: ENC[AES256_GCM,data:GWSHUDfEZmLbKzU02U+ImCR0nVIp5ebngWVKBOmjOjY=,iv:uG7/4QPD8Wd2Q6lqPgKTD533GdbkXjdtZqJnshmNzjI=,tag:jhj5L86v0ZFalyhfQOTgCA==,type:str]
sops:
age:
- recipient: age1zmr32r5mcgyh73asvzl8l690pzwqsx9hle27u3cydngsgv56wykq3v67px
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuSXZWTzloNTMzWk5samRU
amhURmFvdDlSMjd1RVFjUWxuWEtoVE13WUNJClptVUgxblRWL1RIYmZPZ2x2MG1H
Y3ovWWlwL2dLeW9xbndXWEMvQVZONGcKLS0tIGxZNDZkRDBkZ0NDVWxMNGxoUFR4
VG1wSm5wUWRVUEZ3cWlMb2lBNTVkZGcKAa4/S8EWBMt+uvT/6hYRl1GascDul+ox
qlSV/20eA9uamssuId63ewaO4hL7ekwKwqM5cAFH+wx2dno9Ajuang==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-11-04T11:21:36Z"
mac: ENC[AES256_GCM,data:r7iq6iTFL70Zat0sVw9nAHiueZNUWzV2j6q6POBUjeGLqn53ygJAXqG31REBLNKBwabLxsNlTmZRujpP6ySkbFve6fN1C6+6HFWdHnkeJx1nk4iJTvb5+Ld7Exp8yt6b46M/hHZxOffO23jVHuAa47hZ4lHphFpSrlgif79W/CI=,iv:bP9ll+tuTFOfh3kE7xiXtF0cH0sCfhaIs9fhNt0JoEU=,tag:hpBZBwRZxyAgn20aaGoGWA==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.11.0
@@ -0,0 +1,63 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: map-monitor
namespace: iot
spec:
interval: 30m
chartRef:
kind: OCIRepository
name: map-monitor
install:
remediation:
retries: -1
upgrade:
remediation:
retries: 5
driftDetection:
mode: enabled
values:
defaultPodOptions:
securityContext:
runAsUser: 65534
runAsGroup: 65534
runAsNonRoot: true
controllers:
map-monitor:
containers:
app:
image:
repository: python
tag: 3.12-slim
command:
- sh
- -c
- pip install --no-cache-dir --target /tmp/site -r /app/requirements.txt && exec python /app/map_mqtt.py
env:
PYTHONPATH: /tmp/site
PYTHONUNBUFFERED: "1"
PYTHONDONTWRITEBYTECODE: "1"
PIP_DISABLE_PIP_VERSION_CHECK: "1"
HOME: /tmp
envFrom:
- secretRef:
name: map-monitor-secrets
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 128Mi
persistence:
code:
type: configMap
name: map-monitor-code
globalMounts:
- path: /app
readOnly: true
tmp:
type: emptyDir
globalMounts:
- path: /tmp
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: map-monitor
namespace: iot
spec:
interval: 1h
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- secret.enc.yaml
- helmrelease.yaml
configMapGenerator:
- name: map-monitor-code
namespace: iot
files:
- src/map_mqtt.py
- src/map_read.py
- src/requirements.txt
generatorOptions:
disableNameSuffixHash: true
@@ -0,0 +1,36 @@
apiVersion: v1
kind: Secret
metadata:
name: map-monitor-secrets
namespace: iot
stringData:
SERIAL_HOST: ENC[AES256_GCM,data:6cCm6shCy2kymQ==,iv:mqUOG6RZ6HGh3dBWy2LdWWXklU2FFhra9Xi3VgFdQi4=,tag:sJq/S+QslUWxeuK1JgQuBA==,type:str]
SERIAL_PORT: ENC[AES256_GCM,data:ZiqkpB0=,iv:SryQv6eUrTZwA896oG83xHeLjzABqnn4pwlDPA56S3A=,tag:WPmz9uw+G+QQZW4L/Qfm1Q==,type:str]
SERIAL_POLL_INTERVAL: ENC[AES256_GCM,data:Cw==,iv:myGXUNhVKALsedj1Do56clisSiAvqE19rqTGQjtYxCI=,tag:XvTcCvI9B5YyizJ4skfB1A==,type:str]
SERIAL_TIMEOUT: ENC[AES256_GCM,data:mMtT,iv:nz7nJXuLU3tpO2l04+aRTczCwnmgubLOw7a/cg6Yjic=,tag:nMed9+RH6VKZDQURwuMd/g==,type:str]
MQTT_HOST: ENC[AES256_GCM,data:qyjHpa6fNG+IaQvj6LsUGF8iPxWU4NzdzPCt1ZcodFE=,iv:dxCP9gfasYovuBI6pxkynKkNAh99bUIEr8fn9Qbqxww=,tag:mz1BgruIsUXE/sJdiCqH6g==,type:str]
MQTT_PORT: ENC[AES256_GCM,data:PPxXlQ==,iv:s4b8gCGna5Va0B9BN02ZLlofYlIQiVn9KU8qJNECK5I=,tag:neXH/dwhfW1u1UfkRuY2mg==,type:str]
MQTT_USER: ENC[AES256_GCM,data:Xs6v,iv:DlyX9ErjbcJ4WxQFBhCdiBCRUqOpEYwWSg/X3uA4sSk=,tag:4N/85y5h2u754qP4iyIeiw==,type:str]
MQTT_PASSWORD: ENC[AES256_GCM,data:CD/cSnauAZ29KHuwGblqqDxSdrw=,iv:1mUsaq847o4rjbMIpSRErVIF8ZI+4oS4LDJGwbas0Jo=,tag:y7uSxXNFFM5HD/1IMRj5og==,type:str]
MQTT_TLS: ENC[AES256_GCM,data:lhmlb+8=,iv:mZiHwfgmkpWiBBRF5SvuSP/wlXMnAQmNnSZTYc0kMRM=,tag:VNxesMInF1Vj/gbeK/FlJA==,type:str]
MQTT_CLIENT_ID: ENC[AES256_GCM,data:Epdn4DGpQtgl53sc+Qo=,iv:gjVPQA1g0IFrzNJuqOWAAUgaFZsnZ2te5V32Q/SPTJg=,tag:zSauVShmjG0K8FDVevvolQ==,type:str]
MQTT_DISCOVERY_PREFIX: ENC[AES256_GCM,data:9ThR6rD3yXShgW1TmA==,iv:yn+89NBc6rXqbUnnKSqiWhzCruf0HPt3DfgxrhGZq7g=,tag:YoWynvyWRxdJ+UCbfjCJtQ==,type:str]
MQTT_BASE_TOPIC: ENC[AES256_GCM,data:Y8fGesOJmsXOvdE0khg=,iv:CyyDnQkUzGivWWAQnzWyElqoLeXmT7uLns5te03Hq0E=,tag:5CCuHp2ZeaZyXF4LmoLTUg==,type:str]
DEVICE_ID: ENC[AES256_GCM,data:RF2wLrHe/+CA0em/Yc0=,iv:IQx6zlX31rywQlhxchmGu3pjYg7JJlyNw1KT1/ELsL4=,tag:wVQ2s6XDN1l+tM9EQnjX/g==,type:str]
DEVICE_NAME: ENC[AES256_GCM,data:fHTyCXeA0aDlNqp7HRCuppTGkYnP,iv:wMp+umM2Y6S/54t5nc4h9PYCZd5NloOYIpCp3/4vOoE=,tag:PcP3e3hrJ+r78oPC7etm5g==,type:str]
LOG_LEVEL: ENC[AES256_GCM,data:/oGKyg==,iv:3A/971xbMz+dm+czrslz2nS3J1PRaM+7zLNACEHX7OM=,tag:OmLgstVHAT4GpviplU5V8Q==,type:str]
sops:
age:
- recipient: age1k4j5vyqqt7w7k52gvkpfe6znqr9mlgt076l7xgdtyc75lamsh4aqshj5qs
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZZ0R5UzhmOXV6T01KbTdr
Y2tCR2owUHZVV2VVakpEWXZjK3BNcTkzOVdZCjAzUnJaK2lMTWFhS0tTY2EvSU9u
OGh4SlI0em9YYUxkTGZZOUR2SG0yZjAKLS0tIFE5MWt4b3VWcmtVWXpwWjlTbHlT
R3ZQckkwMVJTekNRZW1uQVZVYXAvcTQKTeXGo0pBSFXderktCrgX6/hDRBraiOMF
SPr6hDe0ejMfWyOUSA9XQRQnn20gssRtA5DzNfpkjA3Gam+CU9Lu0w==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-10-26T11:29:23Z"
mac: ENC[AES256_GCM,data:jVnj8WpNjbFHwDfU2NBCcoqyfRYotmvc7w1b2A7e40GPYBKmkqVkZUlxBp4oc2p4qj+SRfPPKSUqDfxNqIS+oiqidui41/EhsJtPBjqyjoWmLfMMriwYUMlxrdFeCNNL97cUJin+Cy/OvPXgIU1BX9hnqyz0mTU5ce1zWju25Mc=,iv:4GJialKu0t9WmGoxKAA1P6wuan5gD8fSn3sToEL0QRU=,tag:yZbaQpIfUFh5eZvWQlKwxg==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.11.0
@@ -0,0 +1,10 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY map_read.py map_mqtt.py ./
ENV PYTHONUNBUFFERED=1
USER nobody
CMD ["python", "map_mqtt.py"]
@@ -0,0 +1,259 @@
#!/usr/bin/env python3
"""
Service: polls a MAP inverter via ser2net and publishes to MQTT for Home Assistant
(MQTT discovery). Values are also written to the console for diagnostics.
All configuration is done via environment variables (see .env.example).
"""
import json
import logging
import os
import signal
import sys
import threading
import paho.mqtt.client as mqtt
from map_read import MapClient, MapError, read_status
log = logging.getLogger("map_mqtt")
def env(name, default=None, cast=str):
v = os.environ.get(name, "")
if v == "":
if default is None:
sys.exit("Environment variable %s is not set" % name)
return default
return cast(v)
SERIAL_HOST = env("SERIAL_HOST", "10.0.64.24")
SERIAL_PORT = env("SERIAL_PORT", 33333, int)
SERIAL_TIMEOUT = env("SERIAL_TIMEOUT", 1.0, float)
SERIAL_POLL_INTERVAL = env("SERIAL_POLL_INTERVAL", 5.0, float)
MQTT_HOST = env("MQTT_HOST")
MQTT_PORT = env("MQTT_PORT", 1883, int)
MQTT_USER = env("MQTT_USER", "")
MQTT_PASSWORD = env("MQTT_PASSWORD", "")
MQTT_CLIENT_ID = env("MQTT_CLIENT_ID", "map-inverter")
MQTT_TLS = env("MQTT_TLS", "false").lower() in ("1", "true", "yes")
DISCOVERY_PREFIX = env("MQTT_DISCOVERY_PREFIX", "homeassistant")
BASE_TOPIC = env("MQTT_BASE_TOPIC", "map_inverter")
DEVICE_ID = env("DEVICE_ID", "map_inverter")
DEVICE_NAME = env("DEVICE_NAME", "MAP Energy")
STATE_TOPIC = "%s/state" % BASE_TOPIC
AVAIL_TOPIC = "%s/availability" % BASE_TOPIC
HA_STATUS_TOPIC = "%s/status" % DISCOVERY_PREFIX
# (key, component, name, unit, device_class, state_class, extra)
ENTITIES = [
("mode_text", "sensor", "Mode", None, None, None, {"icon": "mdi:state-machine"}),
("mode", "sensor", "Mode code", None, None, None,
{"icon": "mdi:numeric", "entity_category": "diagnostic"}),
("u_acc", "sensor", "Battery voltage", "V", "voltage", "measurement", {}),
("i_acc", "sensor", "Battery current", "A", "current", "measurement", {}),
("p_acc", "sensor", "Battery power", "W", "power", "measurement", {}),
("p_load", "sensor", "Load power", "W", "power", "measurement", {}),
("u_net", "sensor", "Grid voltage", "V", "voltage", "measurement", {}),
("i_net", "sensor", "Grid current", "A", "current", "measurement", {}),
("p_net", "sensor", "Grid power", "W", "power", "measurement", {}),
("f_net", "sensor", "Grid frequency", "Hz", "frequency", "measurement", {}),
("u_out", "sensor", "Output voltage", "V", "voltage", "measurement", {}),
("f_out", "sensor", "Output frequency", "Hz", "frequency", "measurement", {}),
("t_acc", "sensor", "Battery temperature", "°C", "temperature", "measurement", {}),
("t_tor", "sensor", "Transformer temperature", "°C", "temperature", "measurement", {}),
("t_trans", "sensor", "Transistor temperature", "°C", "temperature", "measurement", {}),
("u_net_limit", "sensor", "Last out-of-range grid voltage", "V", "voltage", None,
{"entity_category": "diagnostic"}),
("e_net", "sensor", "Energy from grid", "kWh", "energy", "total_increasing", {}),
("e_acc", "sensor", "Energy from battery", "kWh", "energy", "total_increasing", {}),
("e_acc_charge", "sensor", "Battery charge energy", "kWh", "energy", "total_increasing", {}),
("errors_text", "sensor", "Errors", None, None, None,
{"icon": "mdi:alert-circle-outline", "entity_category": "diagnostic"}),
("problem", "binary_sensor", "Problem", None, "problem", None,
{"entity_category": "diagnostic"}),
("net_reverse", "binary_sensor", "Grid export", None, None, None,
{"icon": "mdi:transmission-tower-export"}),
("relay1", "binary_sensor", "Relay 1", None, None, None, {"icon": "mdi:electric-switch"}),
("relay2", "binary_sensor", "Relay 2", None, None, None, {"icon": "mdi:electric-switch"}),
]
def to_payload(s):
"""Flatten the read_status result into JSON for the state topic."""
p = {k: s[k] for k in ("mode", "mode_text", "u_acc", "i_acc", "p_acc", "p_load", "u_net",
"i_net", "p_net", "f_net", "u_out", "f_out", "t_acc", "t_tor",
"t_trans", "u_net_limit")}
# counter unit is 0.01 kWh (malina html/meters.js; confirmed against integrated grid power)
p["e_net"] = s["e_net_raw"] / 100
p["e_acc"] = s["e_acc_raw"] / 100
p["e_acc_charge"] = s["e_acc_charge_raw"] / 100
for k in ("net_reverse", "relay1", "relay2"):
p[k] = "ON" if s[k] else "OFF"
p["problem"] = "ON" if s["errors"] else "OFF"
texts = ["%s: %s" % (name, "; ".join(e["bits"])) for name, e in s["errors"].items()]
p["errors_text"] = (" | ".join(texts) or "none")[:255]
p["errors"] = {name: e["value"] for name, e in s["errors"].items()}
return p
def discovery_messages():
device = {
"identifiers": [DEVICE_ID],
"name": DEVICE_NAME,
"manufacturer": "microART",
"model": "MAP",
}
for key, component, name, unit, dev_class, state_class, extra in ENTITIES:
cfg = {
"name": name,
"unique_id": "%s_%s" % (DEVICE_ID, key),
"state_topic": STATE_TOPIC,
"value_template": "{{ value_json.%s }}" % key,
"availability_topic": AVAIL_TOPIC,
"device": device,
}
if unit:
cfg["unit_of_measurement"] = unit
if dev_class:
cfg["device_class"] = dev_class
if state_class:
cfg["state_class"] = state_class
if key == "errors_text":
cfg["json_attributes_topic"] = STATE_TOPIC
cfg["json_attributes_template"] = "{{ value_json.errors | tojson }}"
cfg.update(extra)
topic = "%s/%s/%s/%s/config" % (DISCOVERY_PREFIX, component, DEVICE_ID, key)
yield topic, json.dumps(cfg, ensure_ascii=False)
class Publisher:
def __init__(self):
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=MQTT_CLIENT_ID)
if MQTT_USER:
self.client.username_pw_set(MQTT_USER, MQTT_PASSWORD)
if MQTT_TLS:
self.client.tls_set()
self.client.will_set(AVAIL_TOPIC, "offline", qos=1, retain=True)
self.client.on_connect = self._on_connect
self.client.on_disconnect = self._on_disconnect
self.client.on_message = self._on_message
self.online = False
self._connack = threading.Event()
self._connect_error = None
def start(self, timeout=10):
"""Connect to the broker; raise on failure (the program will exit).
Later disconnects are handled by paho's automatic reconnect."""
self.client.connect(MQTT_HOST, MQTT_PORT, keepalive=60) # OSError on a bad address
self.client.loop_start()
if not self._connack.wait(timeout):
self.client.loop_stop()
raise ConnectionError("broker did not respond within %d s" % timeout)
if self._connect_error:
self.client.loop_stop()
raise ConnectionError("broker refused connection: %s" % self._connect_error)
def stop(self):
self.client.publish(AVAIL_TOPIC, "offline", qos=1, retain=True).wait_for_publish(2)
self.client.disconnect()
self.client.loop_stop()
def _on_connect(self, client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
log.error("MQTT: broker refused connection: %s", reason_code)
if not self._connack.is_set():
self._connect_error = reason_code
self._connack.set()
return
self._connack.set()
log.info("MQTT: connected to %s:%d", MQTT_HOST, MQTT_PORT)
client.subscribe(HA_STATUS_TOPIC)
self.publish_discovery()
self.set_available(self.online)
def _on_disconnect(self, client, userdata, flags, reason_code, properties):
log.warning("MQTT: disconnected (%s), reconnecting...", reason_code)
def _on_message(self, client, userdata, msg):
# Home Assistant restarted - republish discovery
if msg.topic == HA_STATUS_TOPIC and msg.payload == b"online":
log.info("MQTT: Home Assistant online, republishing discovery")
self.publish_discovery()
self.set_available(self.online)
def publish_discovery(self):
for topic, payload in discovery_messages():
self.client.publish(topic, payload, qos=1, retain=True)
def set_available(self, online):
self.online = online
self.client.publish(AVAIL_TOPIC, "online" if online else "offline", qos=1, retain=True)
def publish_state(self, payload):
self.client.publish(STATE_TOPIC, json.dumps(payload, ensure_ascii=False), qos=0)
def format_line(p):
return ("mode=%d (%s) Ubat=%.1fV Ibat=%.1fA Pbat=%dW Pload=%dW Ugrid=%dV Igrid=%.1fA "
"Pgrid=%dW Fgrid=%sHz Uout=%dV Fout=%sHz Tbat=%d Ttr=%d Ttrans=%d "
"Egrid=%.2f Ebat=%.2f Echarge=%.2f errors=%s") % (
p["mode"], p["mode_text"], p["u_acc"], p["i_acc"], p["p_acc"], p["p_load"],
p["u_net"], p["i_net"], p["p_net"], p["f_net"], p["u_out"], p["f_out"],
p["t_acc"], p["t_tor"], p["t_trans"], p["e_net"], p["e_acc"], p["e_acc_charge"],
p["errors_text"])
def main():
logging.basicConfig(level=env("LOG_LEVEL", "INFO").upper(), stream=sys.stdout,
format="%(asctime)s %(levelname)s %(message)s")
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", line_buffering=True)
stop = threading.Event()
signal.signal(signal.SIGTERM, lambda *a: stop.set())
signal.signal(signal.SIGINT, lambda *a: stop.set())
log.info("Starting: ser2net %s:%d, poll every %.1f s, MQTT %s:%d, topic %s",
SERIAL_HOST, SERIAL_PORT, SERIAL_POLL_INTERVAL, MQTT_HOST, MQTT_PORT, STATE_TOPIC)
pub = Publisher()
try:
pub.start()
except OSError as e: # ConnectionError is an OSError too
log.error("MQTT: failed to connect to %s:%d: %s", MQTT_HOST, MQTT_PORT, e)
sys.exit(1)
client = None
while not stop.is_set():
try:
if client is None:
client = MapClient(SERIAL_HOST, SERIAL_PORT, SERIAL_TIMEOUT)
client.connect()
log.info("ser2net: connected to %s:%d", SERIAL_HOST, SERIAL_PORT)
payload = to_payload(read_status(client))
log.info(format_line(payload))
pub.publish_state(payload)
if not pub.online:
pub.set_available(True)
except (MapError, OSError) as e:
log.error("ser2net: %s", e)
if client is not None:
client.close()
client = None
if pub.online:
pub.set_available(False)
stop.wait(SERIAL_POLL_INTERVAL)
log.info("Stopping")
if client is not None:
client.close()
pub.stop()
if __name__ == "__main__":
main()
@@ -0,0 +1,385 @@
#!/usr/bin/env python3
"""
Read the current operating parameters of a MAP inverter (microART) via ser2net (raw TCP).
Protocol reverse-engineered from thirdparty/malina/usr/bin/mapd.c:
* 19200 8N1 (configured on the ser2net side);
* every byte sent by the host is echoed back by the MAP; every byte of the MAP
reply must be echoed back by the host, otherwise the MAP won't send the next one;
* request: cmd('r'=0x72) | count-1 | addr_hi | addr_lo | crc | '\\n'
* reply: 'o'(0x6F) | data... | crc | '\\n' ('e'=0x65 means error)
* byte 0x0A inside a frame is encoded as DB DC, byte 0xDB as DB DD;
* crc = (0x100 - sum of all frame bytes in encoded form) & 0xFF;
if crc == 0x0A it also serves as the frame terminator.
Usage:
python map_read.py # single read, 10.0.64.24:33333
python map_read.py --loop 2 # poll every 2 s
python map_read.py --json # JSON output
python map_read.py --host 1.2.3.4 --port 4001
The ser2net port must be in raw mode (not telnet), for example:
33333:raw:0:/dev/ttyUSB0:19200 8DATABITS NONE 1STOPBIT
"""
import argparse
import json
import select
import socket
import sys
import time
DEFAULT_HOST = "10.0.64.24"
DEFAULT_PORT = 33333
CMD_READ = 0x72 # 'r'
ANS_OK = 0x6F # 'o'
ANS_ERR = 0x65 # 'e'
LF = 0x0A
ESC = 0xDB
MODES = {
0: "System: OFF, Input grid: OFF",
1: "System: OFF, Input grid: ON",
2: "System: ON, Input grid: OFF, Local grid: BATTERY, Battery: DISCHARGING",
3: "System: ON, Input grid: ON, Local grid: PASS-THROUGH",
4: "System: ON, Input grid: ON, Local grid: PASS-THROUGH, Battery: CHARGING",
}
# Error bit field descriptions (from html/local/RU/local.inc)
ERROR_BITS = {
"RSErrJobM": [
"Battery fully discharged, voltage critically low",
"Battery overvoltage, inverting/charging suspended",
"Battery short circuit while charging, charging suspended",
"Battery short circuit, inverting/charging suspended",
"Main relay may be stuck",
"Short circuit on 220V grid",
"External voltage on 220V output, inverting disabled",
"Firmware reset (interference)",
],
"RSErrJob": [
"Battery discharged, inverting will stop in 1 minute",
"Battery overload",
"Load above rated power (limited operating time)",
"Overheating, inverting/charging suspended",
"Fan not spinning",
"Grid overload, switching to inverter",
"Operating mode failure (power loss/interference)",
"Shutdown after repeated short circuits while charging (cleared after 2 h)",
],
"RSWarning": [
"No action for START+CHARGE button combination",
"Grid voltage out of range",
"External voltage on output or voltage spikes from load",
"Voltage spikes on grid input",
"START or CHARGE button stuck",
"Charging not possible - no grid",
"Load power above EE_LCD_NetMaxPow, switching to inverter",
"Grid unstable",
],
"RSErrDop": [
"Invalid data from previous phase / no link",
"Phase shift outside 60 deg synchronization window",
"BMS I2C error",
"MPPT I2C error",
"Outdated MPPT firmware",
"Parallel MAP synchronization error",
"Slave MAP has no grid while master switched to grid",
None,
],
"I2C_Err": ["I2C Err Ack", "I2C Err Sum", "I2C Err Data Size", "I2C Err Protocol",
None, None, None, None],
"F_Acc_Over": [
"Shutdown: battery overcurrent",
"Shutdown: battery overcurrent while charging",
"Shutdown: battery voltage critically low",
"Shutdown: fan failure",
"Shutdown: power above rated for more than 30 min",
"Shutdown: battery fully discharged",
"Shutdown: temperature sensor out of range",
"Full shutdown after repeated battery overloads (press START)",
],
"F_Net_Over": [
"Inverting disabled - external voltage on output or faulty relay",
"Switched to inverter - grid overload",
None,
"Shutdown after repeated grid overloads (press START)",
None, None, None, None,
],
}
class MapError(Exception):
pass
class MapClient:
def __init__(self, host, port, timeout=1.0):
self.host = host
self.port = port
self.timeout = timeout
self.sock = None
def connect(self):
self.sock = socket.create_connection((self.host, self.port), timeout=5)
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
self.sock.settimeout(self.timeout)
self.flush()
def close(self):
if self.sock:
self.sock.close()
self.sock = None
def __enter__(self):
self.connect()
return self
def __exit__(self, *exc):
self.close()
# --- byte level ------------------------------------------------------
def flush(self):
"""Discard everything accumulated in the input buffer."""
while True:
r, _, _ = select.select([self.sock], [], [], 0.05)
if not r:
return
if not self.sock.recv(4096):
raise MapError("connection closed by ser2net")
def _recv1(self):
try:
b = self.sock.recv(1)
except socket.timeout:
raise MapError("MAP response timeout")
if not b:
raise MapError("connection closed by ser2net")
return b[0]
def _put(self, a):
"""Send a byte and wait for its echo (resyncing to the stream, as mapd.c does)."""
self.sock.sendall(bytes([a]))
for _ in range(21):
if self._recv1() == a:
return
raise MapError("no echo from MAP (0x%02X)" % a)
def _put_coded(self, a, csum):
if a == LF:
seq = (ESC, 0xDC)
elif a == ESC:
seq = (ESC, 0xDD)
else:
seq = (a,)
for b in seq:
self._put(b)
csum += b
return csum
# --- frames ----------------------------------------------------------
def _send_command(self, cmd, addr, count):
csum = 0
for b in (cmd, count - 1, addr >> 8, addr & 0xFF):
csum = self._put_coded(b, csum)
crc = (-csum) & 0xFF
self._put(crc)
if crc != LF:
self._put(LF)
def _read_answer(self):
raw = bytearray()
while True:
b = self._recv1()
raw.append(b)
self.sock.sendall(bytes([b])) # echo back to the MAP
if b == LF or len(raw) > 600:
break
if raw[0] == ANS_ERR:
raise MapError("MAP returned an error ('e')")
if raw[0] != ANS_OK or len(raw) < 3:
raise MapError("unexpected reply: %s" % raw[:16].hex())
if ((-sum(raw[:-2])) & 0xFF) == raw[-2]:
body = raw[1:-2]
elif ((-sum(raw[:-1])) & 0xFF) == LF:
body = raw[1:-1] # crc == 0x0A is itself the frame terminator
else:
raise MapError("checksum error")
data = bytearray()
i = 0
while i < len(body):
if body[i] == ESC and i + 1 < len(body) and body[i + 1] in (0xDC, 0xDD):
data.append(LF if body[i + 1] == 0xDC else ESC)
i += 2
else:
data.append(body[i])
i += 1
return bytes(data)
def read(self, addr, count, retries=3):
"""Read count (1..256) bytes of MAP memory starting at addr."""
last = None
for _ in range(retries):
try:
self._send_command(CMD_READ, addr, count)
data = self._read_answer()
if len(data) < count:
raise MapError("short reply: %d of %d bytes" % (len(data), count))
return data[:count]
except MapError as e:
last = e
time.sleep(0.2)
self.flush()
raise last
# --- data decoding -------------------------------------------------------
def decode_bits(name, value):
texts = ERROR_BITS.get(name, [])
out = []
for bit in range(8):
if value >> bit & 1:
t = texts[bit] if bit < len(texts) and texts[bit] else "bit %d" % bit
out.append(t)
return out
def phase_current(lo, hi):
v = (lo + (hi & 0x7F) * 256) / 10
return -v if hi & 0x80 else v
def read_status(client):
m = client.read(0x400, 256)
x = lambda addr: m[addr - 0x400]
e = client.read(0x527, 96)
y = lambda addr: e[addr - 0x527]
unet = x(0x422) + 100 if x(0x422) else 0
uout = x(0x427) + 100 if x(0x427) else 0
inet_raw = x(0x423)
hz = lambda t: round(6250 / t, 1) if t else None
s = {
"mode": x(0x400),
"mode_text": MODES.get(x(0x400), "unknown mode"),
"status_char": x(0x402),
"u_acc": (x(0x405) * 256 + x(0x406)) / 10,
"i_acc": round(x(0x432) / 16 + x(0x433) * 16, 1),
"i_acc_coarse": x(0x408) * 2,
"p_load": x(0x409) * 100,
"u_net": unet,
"i_net": round(x(0x431) / (16 if inet_raw < 16 else 4), 1),
"p_net": x(0x424) * 100,
"f_net": hz(x(0x425)),
"u_out": uout,
"f_out": hz(x(0x426)),
"u_net_limit": x(0x429) + 100,
"f_net_limit": round(2500 / x(0x428), 1) if x(0x428) else None,
"t_acc": x(0x42E) - 50,
"t_tor": x(0x42F) - 50,
"t_trans": x(0x430) - 50,
"net_reverse": (x(0x443) + x(0x444) * 256) > 32768,
"e_net_raw": x(0x44D) | x(0x44E) << 8 | x(0x44F) << 16,
"e_acc_raw": x(0x450) | x(0x451) << 8 | x(0x452) << 16,
"e_acc_charge_raw": x(0x453) | x(0x454) << 8 | x(0x455) << 16,
"flag_eco": y(0x585),
"relay1": bool(y(0x586) & 1),
"relay2": bool(y(0x586) & 2),
# meaningful only in a three-phase setup, otherwise these cells hold garbage
"i_phase": [phase_current(y(0x528), y(0x529)),
phase_current(y(0x52A), y(0x52B)),
phase_current(y(0x52C), y(0x52D))],
"errors": {},
}
s["p_acc"] = round(s["u_acc"] * s["i_acc"])
for name, addr in (("RSErrSis", 0x42A), ("RSErrJobM", 0x42B), ("RSErrJob", 0x42C),
("RSWarning", 0x42D), ("RSErrDop", 0x447), ("I2C_Err", 0x45A),
("F_Acc_Over", 0x41C), ("F_Net_Over", 0x41D)):
v = x(addr)
if v:
s["errors"][name] = {"value": v, "bits": decode_bits(name, v)}
return s
def print_status(s):
rows = [
("Mode", "%d - %s" % (s["mode"], s["mode_text"])),
("Battery voltage", "%.1f V" % s["u_acc"]),
("Battery current", "%.1f A (coarse %d A)" % (s["i_acc"], s["i_acc_coarse"])),
("Battery power (calc.)", "%d W" % s["p_acc"]),
("MAP load power", "%d W" % s["p_load"]),
("Grid voltage", "%d V" % s["u_net"]),
("Grid current", "%.1f A" % s["i_net"]),
("Grid power", "%d W%s" % (s["p_net"], " (export to grid)" if s["net_reverse"] else "")),
("Grid frequency", "%s Hz" % s["f_net"]),
("Output voltage", "%d V" % s["u_out"]),
("Output frequency", "%s Hz" % s["f_out"]),
("Battery temp.", "%d °C" % s["t_acc"]),
("Transformer temp.", "%d °C" % s["t_tor"]),
("Transistor temp.", "%d °C" % s["t_trans"]),
("Last out-of-range U", "%d V" % s["u_net_limit"]),
("Relay 1 / Relay 2", "%s / %s" % ("on" if s["relay1"] else "off",
"on" if s["relay2"] else "off")),
("Energy from grid", "%.2f kWh" % (s["e_net_raw"] / 100)),
("Energy from battery", "%.2f kWh" % (s["e_acc_raw"] / 100)),
("Battery charge energy", "%.2f kWh" % (s["e_acc_charge_raw"] / 100)),
]
w =max(len(r[0]) for r in rows)
print(time.strftime("%Y-%m-%d %H:%M:%S"))
for k, v in rows:
print(" %-*s : %s" % (w, k, v))
if s["errors"]:
print(" Errors:")
for name, err in s["errors"].items():
print(" %s = 0x%02X: %s" % (name, err["value"], "; ".join(err["bits"])))
else:
print(" No errors")
def main():
ap = argparse.ArgumentParser(description="Read MAP inverter parameters via ser2net")
ap.add_argument("--host", default=DEFAULT_HOST)
ap.add_argument("--port", type=int, default=DEFAULT_PORT)
ap.add_argument("--timeout", type=float, default=1.0, help="per-byte timeout, s")
ap.add_argument("--loop", type=float, metavar="SEC", help="poll every SEC seconds")
ap.add_argument("--json", action="store_true", help="JSON output")
args = ap.parse_args()
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
with MapClient(args.host, args.port, args.timeout) as c:
while True:
try:
s = read_status(c)
if args.json:
print(json.dumps(s, ensure_ascii=False), flush=True)
else:
print_status(s)
print(flush=True)
except MapError as e:
print("Error: %s" % e, file=sys.stderr)
if not args.loop:
return 1
if not args.loop:
return 0
time.sleep(args.loop)
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
pass
except OSError as e:
print("ser2net connection error: %s" % e, file=sys.stderr)
sys.exit(2)
@@ -0,0 +1 @@
paho-mqtt==2.1.0
+7 -1
View File
@@ -8,6 +8,12 @@ spec:
chartRef: chartRef:
kind: OCIRepository kind: OCIRepository
name: ntfy name: ntfy
install:
remediation:
retries: -1
upgrade:
remediation:
retries: 5
driftDetection: driftDetection:
mode: enabled mode: enabled
values: values:
@@ -16,7 +22,7 @@ spec:
containers: containers:
app: app:
image: image:
repository: binwiederhall/ntfy repository: binwiederhier/ntfy
tag: v2.34.0 tag: v2.34.0
args: args:
- serve - serve
+5 -4
View File
@@ -5,8 +5,9 @@ metadata:
namespace: notify namespace: notify
stringData: stringData:
#ENC[AES256_GCM,data:XneiueSaauWV0MEAzJcs730dwgrSt970o99Jq9iYbAF/RGAXPq0aMhSdNtK4PIgY5nSuVDsaGjXMEu+hF7kX4zWdIQg8MQ2n82fpcNc15RCMsyMNn9WVI1WKr5y7gonz5w==,iv:SPiP6hSzqdBk9WUVKT6TYp3qdZ3gje70CclLXaQNPkw=,tag:H8RtqMaF3bmMNUvfZMJtXQ==,type:comment] #ENC[AES256_GCM,data:XneiueSaauWV0MEAzJcs730dwgrSt970o99Jq9iYbAF/RGAXPq0aMhSdNtK4PIgY5nSuVDsaGjXMEu+hF7kX4zWdIQg8MQ2n82fpcNc15RCMsyMNn9WVI1WKr5y7gonz5w==,iv:SPiP6hSzqdBk9WUVKT6TYp3qdZ3gje70CclLXaQNPkw=,tag:H8RtqMaF3bmMNUvfZMJtXQ==,type:comment]
NTFY_AUTH_USERS: ENC[AES256_GCM,data:Uk7Ho93dF6occuGT2+YgRpsUZubXrq7Bvo1CI03FRABR5iZWKhqa0TEfaC90zYRaef/0gqE40iepx293Vt35C4d3gGDCZRzs6Rz+KV520oxx0+CctDSQLO8sJGb71hcVEWceF3Le46+MZJ3vwMxTb+sJ4wQFh/FPds7gNqv6OTWYtmu950+O/i3GeB/PhAELzKE=,iv:6RIycaAZdGa8E3YVLT5QaeI9Zf0zF/zD6NZrn5uW2Ws=,tag:101j0x8gxpPAQ5QYRM6b5Q==,type:str] NTFY_AUTH_USERS: ENC[AES256_GCM,data:G6VE3ZkkiHU1dFY52J2XVKg4RL55SBTNZRp9DKeUHFJho/PURi+UIzS0gk2377ZAIRdX7gn8DOqfCZ4gvUw40TnlC/wFRAgZO1a6G1WKEZHceUr3cwFGQQhN0pwbc7RfK+amWR4Ov3m/lsNCZjU2KxUps8jNCvnGNR9IxHXBGoNt7PzeNnn6vL5JDBFba3MNsNDUVOm3Q4pTwG9pb1cvQYUU89OMIq/MkZfwotlR0HyEG/8hDiClr9PGJSuVSerL3JmtV10txM2bKRCFRtDb1DtYrRu8EzQ=,iv:IukIdkjr1vRdQJBMI+adN8O0UPo4Vv25gB5vCzim74A=,tag:QYGGERjRr7n1Pww9bg6Oiw==,type:str]
NTFY_AUTH_ACCESS: ENC[AES256_GCM,data:5Ua2XuSZ2MtJNdWFASYze6pluI172iPA1/qoTYmqa2j0,iv:+tk0uiGOHpEIEBX4a9lnsEKpS3njS8FKrEgV2EKN0jU=,tag:ofFrrIPu/u59KprNaIdtWA==,type:str] #ENC[AES256_GCM,data:WDat/Zx7qfe6gY1t8npcfvHz+EpTbtEuHJMoUijlJq5FJEu6nVbrASJOkkWDxfhT,iv:6GX4Jff9YZ27RHaCym1ziSkZiF7VifRkxjleLcYVYHY=,tag:RhsfsYqNc9dyFc53TxAFcw==,type:comment]
NTFY_AUTH_ACCESS: ENC[AES256_GCM,data:Mc4c8bDGwIEUZnx2x1N5IDiLPCdHZw==,iv:LCFN1XXjGfzel/3O4FHumcJpn1DhqdqOhjjpzBSUpYs=,tag:QCqj6hIClhXTnINq+hQuow==,type:str]
sops: sops:
age: age:
- recipient: age1uzs33ep6k3htnuwyyp54y373wkf7urd0qvk5kxcmptr5tt0fffus4v06gn - recipient: age1uzs33ep6k3htnuwyyp54y373wkf7urd0qvk5kxcmptr5tt0fffus4v06gn
@@ -18,7 +19,7 @@ sops:
aGFRSVg5alBnNnA4emxkeFFpZ3BnUFkKUPOqUjkjSl77Xvb3i+THWINbvtzrioqF aGFRSVg5alBnNnA4emxkeFFpZ3BnUFkKUPOqUjkjSl77Xvb3i+THWINbvtzrioqF
QBkQh5SSzq8fWrHoHHp7XHu02+p34dEJMOmEg1rlhu7pYRMNbbaRsQ== QBkQh5SSzq8fWrHoHHp7XHu02+p34dEJMOmEg1rlhu7pYRMNbbaRsQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2026-10-26T08:13:42Z" lastmodified: "2026-10-26T09:44:43Z"
mac: ENC[AES256_GCM,data:LPFcdVDHPXceeN7fpxgOPE6BFvCh2XBWmY7Q/ODACMNh+NtKt2feZ+FJbPaLzXXhwIpYtzcoStWb+eFLNIXRzowmfly87OTV6sYqAVAS4QGk1+E9K4AoepvwRSnCUXjm/uIer6DyTNbJ4CQG5s+mHb2LHvKNRpi3R/2v1OHDAJw=,iv:pos1aa5t5KtDvVftKJxKjl3J2i50rsRmElJ6Qp0v/Q8=,tag:FXR0puuma8eU7/U0zkzlOw==,type:str] mac: ENC[AES256_GCM,data:0z4ety9M00u1vIGn1NyqO9K42IGOQt28L08sKU4D3aB6sroqbY6UkafabBYGyzB7LCaOc3iukDCFtB8hBwg6UarNPbeQlZqG3yuHmVuNB2vOubE4U1YFxeTJ5bNvyP4kpSp6DwUiC9QeJtQ0T98aqj9R7dB3x4vU1s/hPmYSLxU=,iv:+5kSIP8prdAOaYCUhQzUlugncFH54tTMweMfo+tP4BY=,tag:bpjq0X3gCy+LbLeSmIX5Jw==,type:str]
encrypted_regex: ^(data|stringData)$ encrypted_regex: ^(data|stringData)$
version: 3.11.0 version: 3.11.0
+16 -1
View File
@@ -10,7 +10,8 @@ spec:
- 10.0.64.6/32 - 10.0.64.6/32
# exclude 10.0.64.7, its in dmz-pool # exclude 10.0.64.7, its in dmz-pool
# exclude 10.0.64.8, its in mqtt-pool # exclude 10.0.64.8, its in mqtt-pool
- 10.0.64.9-10.0.64.19 # exclude 10.0.64.9, its in public-ports-pool
- 10.0.64.10-10.0.64.19
autoAssign: true autoAssign: true
serviceAllocation: serviceAllocation:
serviceSelectors: serviceSelectors:
@@ -62,6 +63,19 @@ spec:
- matchLabels: - matchLabels:
loadBalancerPool: mqtt loadBalancerPool: mqtt
--- ---
# ip for services that need individual ports forwarded from WAN router (e.g. jitsi jvb udp)
# not auto-assigned: a service gets it only with an explicit annotation
# metallb.io/address-pool: public-ports-pool
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: public-ports-pool
namespace: metallb-system
spec:
addresses:
- 10.0.64.9/32
autoAssign: false
---
apiVersion: metallb.io/v1beta1 apiVersion: metallb.io/v1beta1
kind: L2Advertisement kind: L2Advertisement
metadata: metadata:
@@ -73,3 +87,4 @@ spec:
- dmz-pool - dmz-pool
- mqtt-pool - mqtt-pool
- nfs-pool - nfs-pool
- public-ports-pool
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: iot
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: video
+21
View File
@@ -0,0 +1,21 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: jitsi
namespace: flux-system
spec:
interval: 10m
sourceRef:
kind: GitRepository
name: flux-system
path: ./clusters/base/apps/jitsi
prune: true
wait: true
dependsOn:
- name: namespaces
- name: infra
retryInterval: 5s
decryption:
provider: sops
secretRef:
name: sops-age-jitsi
+3 -1
View File
@@ -20,4 +20,6 @@ resources:
- woodpecker.yaml - woodpecker.yaml
- tinyauth.yaml - tinyauth.yaml
- it-tools.yaml - it-tools.yaml
- ntfy.yaml - ntfy.yaml
- map-monitor.yaml
- jitsi.yaml
+21
View File
@@ -0,0 +1,21 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: map-monitor
namespace: flux-system
spec:
interval: 10m
sourceRef:
kind: GitRepository
name: flux-system
path: ./clusters/base/apps/map_monitor
prune: true
wait: true
dependsOn:
- name: namespaces
- name: infra
retryInterval: 5s
decryption:
provider: sops
secretRef:
name: sops-age-map-monitor
+3 -1
View File
@@ -20,4 +20,6 @@ resources:
- ../../base/namespaces/dev.yaml - ../../base/namespaces/dev.yaml
- ../../base/namespaces/ci.yaml - ../../base/namespaces/ci.yaml
- ../../base/namespaces/tools.yaml - ../../base/namespaces/tools.yaml
- ../../base/namespaces/notify.yaml - ../../base/namespaces/notify.yaml
- ../../base/namespaces/iot.yaml
- ../../base/namespaces/video.yaml