Compare commits

..
41 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
azykov@mail.ru c5db75828a ntfy users via env+secret 2026-10-26 11:15:14 +03:00
azykov@mail.ru 66a5d976bf ntfy bump to 2.34.0 2026-10-26 11:14:52 +03:00
azykov@mail.ru a1a9dfdcf4 ntfy 2026-10-26 10:53:42 +03:00
azykov@mail.ru 546212679f replaced it-tools with next-tools fork 2026-10-20 14:06:58 +03:00
azykov@mail.ru 8cf2b66fa1 it-tools app 2026-10-20 13:53:12 +03:00
azykov@mail.ru a27119e04b fixed homeassistant manifest error 2026-10-20 12:21:11 +03:00
azykov@mail.ru 51cf68683d moved all apps from authelia to tinyauth 2026-10-20 12:20:39 +03:00
azykov@mail.ru e0e4232bad longhorn-frontend switched to tinyauth 2026-10-20 12:09:11 +03:00
azykov@mail.ru fce161e13e tinyauth oidc clients and keys 2026-10-20 12:05:05 +03:00
azykov@mail.ru e9b0ae25c7 tinyauth background image replaced 2026-10-20 11:11:58 +03:00
azykov@mail.ru a243ccccdb tinyauth 2026-10-20 10:20:11 +03:00
azykov@mail.ru a29ecfdfa6 make HA claim fix on empty cluster also 2026-10-20 08:51:10 +03:00
azykov@mail.ru dd9c364860 HA pvc name fix 2026-10-20 08:45:23 +03:00
azykov@mail.ru 57ed781366 media dir for HA 2026-10-19 21:12:44 +03:00
azykov@mail.ru b992f47264 longhorn staleReplicaTimeout 2880 2026-10-13 18:06:46 +03:00
azykov@mail.ru d909f3e317 custom longhorn storage class 2026-10-13 14:58:00 +03:00
azykov@mail.ru 406678fdc2 gitea ALLOWED_HOST_LIST to ci.aggtaa.com 2026-10-08 11:16:48 +03:00
azykov@mail.ru dedba652d6 zigbee2mqtt experimental_event_entities:true flag in config map 2026-10-07 22:54:25 +03:00
azykov@mail.ru 1e0908ee90 woodpecker grpc key and persistent agent id 2026-10-07 11:40:34 +03:00
azykov@mail.ru ecf03f2b01 woodpecker automountServiceAccountToken and bump to 3.18.1 2026-10-07 11:31:00 +03:00
azykov@mail.ru 44b33f8030 woodpecker errors fixed 2026-10-07 11:26:41 +03:00
azykov@mail.ru a31372b953 woodpecker ci 2026-10-07 11:23:22 +03:00
azykov@mail.ru b8c581eab8 dev.aggtaa.com/m path 2026-09-29 16:22:33 +03:00
azykov@mail.ru 1af1a3527d dev.aggtaa.com/api/admin path auth, prefix stip 2026-09-29 13:17:55 +03:00
azykov@mail.ru 954a0b4000 dev.aggtaa.com, /, /api, /m to 10.0.64.157 2026-09-29 12:50:05 +03:00
azykov@mail.ru 8494263a3f 1 year retries for NAS mounts on webdav 2026-09-28 10:29:49 +03:00
azykov@mail.ru 050f86e503 webdav HostToContainer mountPropagation 2026-09-28 10:19:10 +03:00
azykov@mail.ru 06d92a034f soft nfs mounts for NAS shares 2026-09-18 10:59:04 +03:00
azykov@mail.ru 60fd88b9c1 authelia image name fix 2026-09-14 22:02:44 +03:00
azykov@mail.ru cbbe199633 longhorn generous readiness probe 5s timeout 2026-09-07 09:46:09 +03:00
azykov@mail.ru a10942bb3c gitea ssh address changed to git-ssh.aggtaa.com. flux points to the new in-cluster gitea 2026-09-07 09:36:09 +03:00
70 changed files with 1980 additions and 17 deletions
+1
View File
@@ -2,3 +2,4 @@ migration-from-old-files/
migration-from-docker/ migration-from-docker/
.decrypted~secret.enc.yaml .decrypted~secret.enc.yaml
gitea-backup/ gitea-backup/
*.agekey
+15
View File
@@ -35,3 +35,18 @@ creation_rules:
- path_regex: gitea.*\.enc\.yaml$ - path_regex: gitea.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$ encrypted_regex: ^(data|stringData)$
age: age1ktlxm0hukw6jxm62txqy5x2nl8hpdnal8nv00kfhrf3pw33574aqa6j5cj age: age1ktlxm0hukw6jxm62txqy5x2nl8hpdnal8nv00kfhrf3pw33574aqa6j5cj
- path_regex: woodpecker.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age13qwkssk8esashln2j3gfv6e9csashr4vddkdav5v38hm5wejl36qd2ew9s
- path_regex: tinyauth.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1rvyt7nlnjqeyt6ykwh9fj9d88kjmq9vetcx667ul3ql6fyfz4gqqh9lq9q
- path_regex: ntfy.*\.enc\.yaml$
encrypted_regex: ^(data|stringData)$
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
+1 -1
View File
@@ -17,7 +17,7 @@ spec:
app: app:
image: image:
repository: authelia/authelia repository: authelia/authelia
tag: release-v4.39.21 tag: 4.39.21
probes: probes:
liveness: liveness:
enabled: true enabled: true
+104
View File
@@ -0,0 +1,104 @@
# Three Ingresses on one host because middlewares and router priority are
# per-Ingress. Traefik matches pathType Prefix character-wise (not per path
# segment), so public prefixes carry a trailing slash and priorities are
# explicit - by default Traefik orders routers by rule length, which would
# not reliably put /api/admin above /api/.
# 300 dev-admin /api/admin -> Authelia -> api (prefix /api stripped)
# 200 dev-public /api/ /m /m/ -> api / m, public (prefixes stripped)
# 100 dev / -> Authelia -> ui (also bare /api)
# All carry the tls section (otherwise the router is http-only), but only
# "dev" has the cert-manager annotation so a single Certificate owns dev-tls.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dev
namespace: dev
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.priority: "100"
traefik.ingress.kubernetes.io/router.middlewares: auth-tinyauth@kubernetescrd
spec:
ingressClassName: traefik
rules:
- host: dev.aggtaa.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: dev
port:
name: ui
tls:
- secretName: dev-tls
hosts:
- dev.aggtaa.com
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dev-admin
namespace: dev
annotations:
traefik.ingress.kubernetes.io/router.priority: "300"
traefik.ingress.kubernetes.io/router.middlewares: auth-tinyauth@kubernetescrd,dev-strip-prefix@kubernetescrd
spec:
ingressClassName: traefik
rules:
- host: dev.aggtaa.com
http:
paths:
# no trailing slash on purpose: also catches bare /api/admin
# (and /api/administrator...), all of which stay behind Authelia
- path: /api/admin
pathType: Prefix
backend:
service:
name: dev
port:
name: api
tls:
- secretName: dev-tls
hosts:
- dev.aggtaa.com
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dev-public
namespace: dev
annotations:
traefik.ingress.kubernetes.io/router.priority: "200"
traefik.ingress.kubernetes.io/router.middlewares: dev-strip-prefix@kubernetescrd
spec:
ingressClassName: traefik
rules:
- host: dev.aggtaa.com
http:
paths:
- path: /api/
pathType: Prefix
backend:
service:
name: dev
port:
name: api
- path: /m
pathType: Exact
backend:
service:
name: dev
port:
name: m
- path: /m/
pathType: Prefix
backend:
service:
name: dev
port:
name: m
tls:
- secretName: dev-tls
hosts:
- dev.aggtaa.com
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- service.yaml
- middleware.yaml
- ingress.yaml
+13
View File
@@ -0,0 +1,13 @@
# Shared by dev-admin and dev-public: /api/a -> /a, /api/admin/b -> /admin/b,
# /m/main.css -> /main.css. Only the first matching prefix is stripped, and
# none of these routes can match both.
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: strip-prefix
namespace: dev
spec:
stripPrefix:
prefixes:
- /api
- /m
+40
View File
@@ -0,0 +1,40 @@
# dev.aggtaa.com backends live outside the cluster on 10.0.64.157:
# selector-less Service + hand-written EndpointSlice point at them.
apiVersion: v1
kind: Service
metadata:
name: dev
namespace: dev
spec:
ports:
- name: ui
port: 10001
- name: api
port: 10002
- name: m
port: 10003
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: dev-external
namespace: dev
labels:
kubernetes.io/service-name: dev
endpointslice.kubernetes.io/managed-by: flux
addressType: IPv4
ports:
- name: ui
port: 10001
protocol: TCP
- name: api
port: 10002
protocol: TCP
- name: m
port: 10003
protocol: TCP
endpoints:
- addresses:
- 10.0.64.157
conditions:
ready: true
+1 -1
View File
@@ -59,7 +59,7 @@ spec:
className: traefik className: traefik
annotations: annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.middlewares: auth-authelia@kubernetescrd traefik.ingress.kubernetes.io/router.middlewares: auth-tinyauth@kubernetescrd
hosts: hosts:
- host: acc.aggtaa.com - host: acc.aggtaa.com
paths: paths:
+2 -1
View File
@@ -24,9 +24,10 @@ spec:
USER_GID: "1000" USER_GID: "1000"
GITEA__server__DOMAIN: git.aggtaa.com GITEA__server__DOMAIN: git.aggtaa.com
GITEA__server__ROOT_URL: https://git.aggtaa.com/ GITEA__server__ROOT_URL: https://git.aggtaa.com/
GITEA__server__SSH_DOMAIN: git.aggtaa.com GITEA__server__SSH_DOMAIN: git-ssh.aggtaa.com
GITEA__server__SSH_PORT: "2222" GITEA__server__SSH_PORT: "2222"
GITEA__server__LFS_START_SERVER: "true" GITEA__server__LFS_START_SERVER: "true"
GITEA__webhook__ALLOWED_HOST_LIST: ci.aggtaa.com
GITEA__security__INSTALL_LOCK: "true" GITEA__security__INSTALL_LOCK: "true"
GITEA__security__PASSWORD_HASH_ALGO: pbkdf2 GITEA__security__PASSWORD_HASH_ALGO: pbkdf2
GITEA__security__INTERNAL_TOKEN: GITEA__security__INTERNAL_TOKEN:
@@ -75,3 +75,11 @@ spec:
retain: true retain: true
globalMounts: globalMounts:
- path: /config - path: /config
media:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 1Gi
storageClass: longhorn
retain: true
globalMounts:
- path: /media
@@ -0,0 +1,57 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: it-tools
namespace: tools
spec:
interval: 10m
chartRef:
kind: OCIRepository
name: it-tools
driftDetection:
mode: enabled
values:
controllers:
it-tools:
containers:
app:
image:
repository: ghcr.io/willjayyyy/next-tools
tag: 1.10.3
probes:
liveness:
enabled: true
readiness:
enabled: true
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 128Mi
service:
app:
controller: it-tools
ports:
http:
port: 80
ingress:
app:
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.middlewares: auth-tinyauth@kubernetescrd
hosts:
- host: tools.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: it-tools-tls
hosts:
- tools.aggtaa.com
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: it-tools
namespace: tools
spec:
interval: 1h
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- helmrelease.yaml
+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
+96
View File
@@ -0,0 +1,96 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: ntfy
namespace: notify
spec:
interval: 30m
chartRef:
kind: OCIRepository
name: ntfy
install:
remediation:
retries: -1
upgrade:
remediation:
retries: 5
driftDetection:
mode: enabled
values:
controllers:
ntfy:
containers:
app:
image:
repository: binwiederhier/ntfy
tag: v2.34.0
args:
- serve
env:
NTFY_BASE_URL: https://notify.aggtaa.com
NTFY_LISTEN_HTTP: ":80"
NTFY_BEHIND_PROXY: "true"
NTFY_CACHE_FILE: /var/lib/ntfy/cache.db
NTFY_AUTH_FILE: /var/lib/ntfy/user.db
NTFY_AUTH_DEFAULT_ACCESS: deny-all
NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments
NTFY_ENABLE_LOGIN: "true"
envFrom:
- secretRef:
name: ntfy-secrets
probes:
liveness:
enabled: true
custom: true
spec:
httpGet:
path: /v1/health
port: 80
readiness:
enabled: true
custom: true
spec:
httpGet:
path: /v1/health
port: 80
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 256Mi
service:
app:
controller: ntfy
ports:
http:
port: 80
ingress:
app:
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: notify.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: ntfy-tls
hosts:
- notify.aggtaa.com
persistence:
data:
type: persistentVolumeClaim
storageClass: longhorn
accessMode: ReadWriteOnce
size: 2Gi
retain: true
globalMounts:
- path: /var/lib/ntfy
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: ntfy
namespace: notify
spec:
interval: 1h
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- helmrelease.yaml
- secret.enc.yaml
+25
View File
@@ -0,0 +1,25 @@
apiVersion: v1
kind: Secret
metadata:
name: ntfy-secrets
namespace: notify
stringData:
#ENC[AES256_GCM,data:XneiueSaauWV0MEAzJcs730dwgrSt970o99Jq9iYbAF/RGAXPq0aMhSdNtK4PIgY5nSuVDsaGjXMEu+hF7kX4zWdIQg8MQ2n82fpcNc15RCMsyMNn9WVI1WKr5y7gonz5w==,iv:SPiP6hSzqdBk9WUVKT6TYp3qdZ3gje70CclLXaQNPkw=,tag:H8RtqMaF3bmMNUvfZMJtXQ==,type:comment]
NTFY_AUTH_USERS: ENC[AES256_GCM,data:G6VE3ZkkiHU1dFY52J2XVKg4RL55SBTNZRp9DKeUHFJho/PURi+UIzS0gk2377ZAIRdX7gn8DOqfCZ4gvUw40TnlC/wFRAgZO1a6G1WKEZHceUr3cwFGQQhN0pwbc7RfK+amWR4Ov3m/lsNCZjU2KxUps8jNCvnGNR9IxHXBGoNt7PzeNnn6vL5JDBFba3MNsNDUVOm3Q4pTwG9pb1cvQYUU89OMIq/MkZfwotlR0HyEG/8hDiClr9PGJSuVSerL3JmtV10txM2bKRCFRtDb1DtYrRu8EzQ=,iv:IukIdkjr1vRdQJBMI+adN8O0UPo4Vv25gB5vCzim74A=,tag:QYGGERjRr7n1Pww9bg6Oiw==,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:
age:
- recipient: age1uzs33ep6k3htnuwyyp54y373wkf7urd0qvk5kxcmptr5tt0fffus4v06gn
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4ZTg3RVF1Y2sxUjAzTVgw
d2ZkQVpmdzNqYzIwRVR1RGExUjM4SVRMSVN3Cis1bEpZNWZkY1VaWDkzUUlnN2RZ
TVUyZ2VNb0MvS3R5c283Ui9DRzkydUEKLS0tIFZMKzhJWnVZUVRsSFNHYStUaGti
aGFRSVg5alBnNnA4emxkeFFpZ3BnUFkKUPOqUjkjSl77Xvb3i+THWINbvtzrioqF
QBkQh5SSzq8fWrHoHHp7XHu02+p34dEJMOmEg1rlhu7pYRMNbbaRsQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-10-26T09:44:43Z"
mac: ENC[AES256_GCM,data:0z4ety9M00u1vIGn1NyqO9K42IGOQt28L08sKU4D3aB6sroqbY6UkafabBYGyzB7LCaOc3iukDCFtB8hBwg6UarNPbeQlZqG3yuHmVuNB2vOubE4U1YFxeTJ5bNvyP4kpSp6DwUiC9QeJtQ0T98aqj9R7dB3x4vU1s/hPmYSLxU=,iv:+5kSIP8prdAOaYCUhQzUlugncFH54tTMweMfo+tP4BY=,tag:bpjq0X3gCy+LbLeSmIX5Jw==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.11.0
@@ -40,7 +40,7 @@ spec:
# behind Authelia the same way fava/zigbee2mqtt are. # behind Authelia the same way fava/zigbee2mqtt are.
annotations: annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.middlewares: auth-authelia@kubernetescrd traefik.ingress.kubernetes.io/router.middlewares: auth-tinyauth@kubernetescrd
hosts: hosts:
- host: sync.aggtaa.com - host: sync.aggtaa.com
paths: paths:
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long
@@ -0,0 +1,108 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: tinyauth
namespace: auth
spec:
interval: 10m
chartRef:
kind: OCIRepository
name: tinyauth
driftDetection:
mode: enabled
values:
controllers:
tinyauth:
containers:
app:
image:
repository: ghcr.io/tinyauthapp/tinyauth
tag: v5.2.0
env:
TZ: Europe/Moscow
TINYAUTH_APPURL: https://login.aggtaa.com
TINYAUTH_UI_TITLE: "Welcome"
TINYAUTH_RESOURCES_PATH: /resources
TINYAUTH_UI_BACKGROUNDIMAGE: /resources/background.webp
TINYAUTH_AUTH_USERSFILE: /config/users
TINYAUTH_AUTH_SECURECOOKIE: "true"
TINYAUTH_LABELPROVIDER: none
TINYAUTH_ANALYTICS_ENABLED: "false"
TINYAUTH_DATABASE_DRIVER: memory
TINYAUTH_OIDC_PRIVATEKEYPATH: /oidc/key.pem
TINYAUTH_OIDC_PUBLICKEYPATH: /oidc/key.pub
TINYAUTH_OIDC_CLIENTS_FILEBROWSER_CLIENTID: filebrowser-quantum
TINYAUTH_OIDC_CLIENTS_FILEBROWSER_NAME: files.aggtaa.com
TINYAUTH_OIDC_CLIENTS_FILEBROWSER_TRUSTEDREDIRECTURIS: https://files.aggtaa.com/api/auth/oidc/callback
TINYAUTH_OIDC_CLIENTS_FILEBROWSER_CLIENTSECRET:
valueFrom:
secretKeyRef:
name: tinyauth-secrets
key: TINYAUTH_OIDC_CLIENTS_FILEBROWSER_CLIENTSECRET
TINYAUTH_OIDC_CLIENTS_MDVAULT_CLIENTID: markdown-vault-mcp
TINYAUTH_OIDC_CLIENTS_MDVAULT_NAME: markdown-vault-mcp
TINYAUTH_OIDC_CLIENTS_MDVAULT_TRUSTEDREDIRECTURIS: https://md-vault.mcp.aggtaa.com/auth/callback
TINYAUTH_OIDC_CLIENTS_MDVAULT_CLIENTSECRET:
valueFrom:
secretKeyRef:
name: tinyauth-secrets
key: TINYAUTH_OIDC_CLIENTS_MDVAULT_CLIENTSECRET
probes:
liveness:
enabled: true
readiness:
enabled: true
resources:
requests:
cpu: 20m
memory: 64Mi
limits:
memory: 256Mi
service:
app:
controller: tinyauth
ports:
http:
port: 3000
ingress:
app:
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: login.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: app
port: http
tls:
- secretName: tinyauth-tls
hosts:
- login.aggtaa.com
persistence:
users:
type: secret
name: tinyauth-secrets
globalMounts:
- path: /config/users
subPath: users
readOnly: true
oidc-key:
type: secret
name: tinyauth-oidc-key
globalMounts:
- path: /oidc
readOnly: true
resources:
type: configMap
name: tinyauth-resources
globalMounts:
- path: /resources
readOnly: true
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: tinyauth
namespace: auth
spec:
interval: 1h
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- helmrelease.yaml
- middleware.yaml
- secret.enc.yaml
- configmap.yaml
- oidc-key.enc.yaml
@@ -0,0 +1,14 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: tinyauth
namespace: auth
spec:
forwardAuth:
address: http://tinyauth.auth.svc.cluster.local:3000/api/auth/traefik
trustForwardHeader: true
authResponseHeaders:
- Remote-User
- Remote-Groups
- Remote-Name
- Remote-Email
@@ -0,0 +1,23 @@
apiVersion: v1
kind: Secret
metadata:
name: tinyauth-oidc-key
namespace: auth
stringData:
key.pem: ENC[AES256_GCM,data:1LGq+HOfsvba2hrMBgwTiaJY4oSXNVRRyBQt7k+Dv5ZunCNbc57qxGIAi259zekb4yqdnP9SHUo0Uzmz1lgOUwhw4pPNq8YmWkjoLthjBB5FaNqilifUzFXYG3+fC4WP0IxHWUFDhB4tgP9zwnc682/QnQBMKzDXAL9bnRf8OTemNLnAjHt/+vbOtW6ySSmjnF+RRS0gz9/CjPwnCwfqqqRoIaiHT2I5nKuzSggGDNRENlGHsUCBypP08ZbuyQ+wRMWNDhtuG8KY8pWxkvOfwAjbJtJ34/xTGeWwCUQ0fs6khMXzMmvnPKmG/HTAAWrgLeMlhicDLB74nBMY7zbjX1y2HzJPVXAnWNE2sbWXhBIhi+/koyRdWSoUoD7flEu1O44cgRJSh2QqEa4cw4NC+tRm0Dm0R37vOEgMHF/D3cKBxSmgKLxUKw+ZCn5aYF2+Dwo5L+HtuI0VnQ5caq0HCrPN17m63gceVS/mbGYT/A9XSvxhWh5hrtzzn1v3xER/HdOEK1GlZBOL2QPfzyRx9Yx05f/NLGghK+Jk/OBtaV8+0ORGfjJFi+uU2tus8gznESTtZGG6Oz25aIYj7cYCNM93szk2q2lle8wCK8DX48wD5LEwS/7zcWJ2J0FeH/+ihIi2N6cRAgT2P5CHATFX4ghQkAH3UZF0oC2x0EfRXmVE9K2twuD70CSqmBIZvSqC1zEb5KpL283E5lsfVZu3RRryplONMaZZM2bIJqwaEq0wqbtvTnKFTWQEA4JRzgy4i8cFyxfq8ELoEacsjVgZCrASvMjK9NmuqUt4dbKMSuIz0S780Om1V0OoshJiGi9Thu7AWIi0WURRi0ZkvaiLDuqwx+VS5Vt+1y6JByUlsKZg4Dg6saYePKEqClD5nqTv4KsVU3psldOdlS/S4MhBCK31RPAI0yZbDF7job1K4pYWpzuJNH3jh/EDqzbBbNpnTnt6VA9KJ6obJ1vzOVAQB804WB0+hM8ap8vcqHLRPhqa7wZ9mJKVkPBxcr0B3ZulXn428FpjEzCLhUPXVNdJsxpPq0LtmN4yHsfGA/BVwXveT2gLxF+cT5IHOpQVR1ZI/I62ebCfupHYPhZ5aU/UFbCykc2PPQTVM5acVKnwbkU9nR8PmrrZxNmgTQ1akrrQoAMkPkKBsE8R3MIH4md4QhH1B7NV79OwJHrShGDajCPPkGnvFP6w+S4ecdwLkutg57Vhf9Gpq81ztzaq1W/W5y1ax4rGowlRU5nOHy5hDfyLUr0O79319oit4LD7VVWzFiBxWFNaky617V9B/xXFqxInGmQAJVPoPkCE9cHCNVbrblAeND36zjcr/tEWYlNCakt3sNFRQTWaO0MoF1+iLxCQvK7dpRpA3IhOPhFdBRj11J9PI+zpeSXZHYXP2XrfYna062KYb7BxiRiCrO2HAtgKY2TTcA8LHYQ+QiNefSqiNSN3zI7vclb4D6RmDpbm8Hg+SZV1KdvBYw/laj0ctNOnTqwZY51foDgjDnhbRcR5M1mbpq3LUY6TUUoV4+blR1cUKk3pQD5eHVEB/gI5r/mLoK/aesDhVsqEsI1fmwzGESJOL2zRXwyzuPrWj65yfcN0dDK/d06o7vnHdaJIPrEaAmX0M+jD92thIrrUMgkD6r6WqtCYKy81tKqBkMIb4M4wO1ttZ/6WjM7LjNfGAeYhMOTI/LWjmHR/JKrAantSgRgsE6hNGA+C8VUQTpO1rDWFNXRb/vxsibFSypi481ydaySw6Qum/f+eLrLGJdIgWCtI3H+L42N80+k5l03pyuJixqCk0kqVrDQmITNdkGJvbqA7uR3YncSOjPIWn7q79KUca4qW+O1CwtgBJYJqSJiDCVwi3tZ6zv7eDBcq7PGrN4R3hcarY9GVo0Vr4YdSN9VU5PkaXFfG10XZyrHBOLmm4/2iy7NxxWY8d4Z3nXUjitEIAkUa/CGhA3WMiqMkkbeLeiRtmbXZ8ZcCBO5FEUSG/z11PyLRi0o5kct3GlaUtl/QRg4SBGx4t3iDf5gHPb298+ulSErL40GjKLyiZh+YfNPDqRdIXBpKTjOand1EZe1yiY3otCj9NVUdrvkSFL6lYXcCQvVtzLIvrfev/QVqpIOf9d0gxp/em4JTZ+YauRNu5fXcnqezncpQAcXy27O4y2+4cko8tk7aoBwcrl/fKp13nx22I62IJGzsLA7BNjRHUG13iddZoBOzhbJ3xlgNR0nNlIYNSQ==,iv:ZO366aMLKSUkaMdlejlGGZ8rLAQITDJ+rtqHIDjHJCk=,tag:m3eUo6FRn5Gmrf/ozMGldQ==,type:str]
key.pub: ENC[AES256_GCM,data:F0YQFFEAwB+bruva4HB4BKeIjAPhnssnd13JLMRMyLMfF8JdIBbjyKcz8NUVq8l5FxFDy3W3jAJmOfVqUwHfajgdT61S2Rj+dBXwV947ruwBiLS3rTnJvgfLIEOf1f4mG2hZH4X7jodInh7wL+IPC7N9Wqmccdfc1eTvn7GjM5KgU6yzsDIdq8CMTOWciRIgLz81sDCP6mLAy4AG+a6nyzhCYSfoQogbl00CP9i6JdV1FcJfrzQY2lgshgrDOynK2bNxwaRP0CVajAn5ccj8sawvIA5EgcEdoasfGGf3WCUnFKDVmYZDZjpeTGDnCRZMFIpKfXbNavaNn2Gwl1icR58UFKsi7tXCePY8MW12gseONFXDoU6OKo+xtgiEk5SPryrskK3m7Dq+9O7No12f2XNhISGlf/sRtY+WmxxXceUTywoOHVRkbcapODRs0svmJBeQZKJ85tE+Fre1l+1mjLGB4ZeaA0euKEZDtTXT95icepMaHIcbZHURwp5jRfYxp7a9VzpWY+m1rwNtnpHe4A16JoU+0ZARPLCs8kX/DqgZDaSy9f+IUjvQYbNTsUNEJCvtMZPWmr/Vz8Je61/8pGz99g==,iv:L5JDiEFJnBPcXGZMmFUVC8Uq12Bb08qYFWhdZCYBivY=,tag:Lpz7BAOaQ1GK9W6WaZ4LLw==,type:str]
sops:
age:
- recipient: age1rvyt7nlnjqeyt6ykwh9fj9d88kjmq9vetcx667ul3ql6fyfz4gqqh9lq9q
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFZGwrK2lEVXBWQnBaeVlx
aUVPQVRyOTB2WE5HdWR2VldUbmQxZTBuTFIwCks1dkc5aGVXWnpIN1NYNVA0em14
c3h4K3hLYkZlU0VhRUEyajFEZzI3UDgKLS0tIFpLQkxlWGtUTGhoVUpiSW9WOStT
blZOcjI3TS84K1hZY0ZxbS95SndsUUEKt3ZYkU2/5uPeU7C72luZYP3tLJJ73ERX
A8eUMsVOI5T7Y6/i4B1OQSt1xFBXkMSJu3YU739fxFumdb8juALxzg==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-10-20T08:48:16Z"
mac: ENC[AES256_GCM,data:IcHXiY7PJpNy/sGirwh7rm8XzyuC/ZfajaxolOgfP0p1bMF6yn2tzgf1vyHJszVEwXE7rLZEriR357PaHyGgkIxDnb76FwLTiw+feEBgpl/77HY1oOnI4yc0LrHh5fkanxadw/krJoQ8JD827Ek9h1LR7ICTJ40EDSpdfwdH/1E=,iv:OXMfJyXrXH4S5v2cTdwJijzE2/4L475ZArA/nw06s48=,tag:ykl0rz2xmhJ9NZIjuU111A==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.11.0
@@ -0,0 +1,24 @@
apiVersion: v1
kind: Secret
metadata:
name: tinyauth-secrets
namespace: auth
stringData:
users: ENC[AES256_GCM,data:C3Dhfymnr+WLWy18EpY9upXn+f7sY6w0cf3oXO9352PTPCP+rLYQTm8eiJtsU8gp/Uea7lMpsRPHSy8CoRdI8DOGT+hyPyK1ovjLdyO7bEiq0TtYOIFlHSR7Kwo0Cm+pZegSN9e1dm/v51+YyFfX3JpLaymYrlmXL3sFPQ0yOzEuMp3zXSV3VAkJe6gZrswIAUNhY0xmaGItr4Vc2isItDuc16lx7tW2wJHI7NgLI98p9FyU8HlXd9MsiPMxDaXSGeAwnjGU+TEvQv9NbhcZO0sUocSi2zgnRbi0i8Zt,iv:5OVryBqH4VNC71QdZkqZsoDrfbT5++Z5xZjgCFNOAu0=,tag:V+OC7Mad6Dw38u91gp6d/Q==,type:str]
TINYAUTH_OIDC_CLIENTS_FILEBROWSER_CLIENTSECRET: ENC[AES256_GCM,data:kBA3jDc+xFyvLNtfPjO+gYtRrT3HgdsoC1MktIgFomA=,iv:JKStNWy9BHcpkSwAG917sC4zzStKcgtU7ZXpKdSraRQ=,tag:VV96+U1EaIQevZHnCDmQag==,type:str]
TINYAUTH_OIDC_CLIENTS_MDVAULT_CLIENTSECRET: ENC[AES256_GCM,data:SiipG4qFp536YfrMqfjeZyIA5hxXfDH1JgGGDrkz0bb2mwdSV2S3szLffxNwibL9xAF/dST6I++N5ErLXGH10w==,iv:hzJyoLO1z4eBuDmBYJ5AfSh4RPggJN07EWeKbP+FFV4=,tag:R46K78XLC0BmQ0n6b+irQA==,type:str]
sops:
age:
- recipient: age1rvyt7nlnjqeyt6ykwh9fj9d88kjmq9vetcx667ul3ql6fyfz4gqqh9lq9q
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJS1llcTdHUmx2ZXdjVFh1
ekJheVN2MVBwa0V1a2JOY2pFM1pmYUJrMzFRCllaRnlnTGFVYTlQeXVxNXFsNEZu
bWUxd3BWZTlEOWxsZVRnREx6YUJ6em8KLS0tIGNDd1Znb0dlSDZEUE1JUkdWMlBG
ajBNVW1pa20zKzhISmFBUmdiazA4T1UK0wyvTMGsyJthx2Py1LITvRePQsjKN/9v
jqKjeeCuVutQCo3FvoVrlkkmiD19jqPDoXjWz95sTN6QjWmP7u6AIA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-10-20T08:39:59Z"
mac: ENC[AES256_GCM,data:pKMx3OwJ5x41WghATDCwDNJADs2KCQkDmDsjfbL6Vx98jFOL+MwvCRujOM2UzzAl8QnNLFddimmpNwoFBifQEbeHTpVnT1uqZkiSV89mM1nIoyYIo3exNoynVUoqp1NXxhfwRrRVC3beF3keTxES//kh8/W3as16R1vgBSLoj6s=,iv:AAiLBicP0tywLWXA67cin51TqV8TBXtlpjazta2PyFs=,tag:wSkJBOIhryUNVlGeKrgmnA==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.11.0
@@ -90,13 +90,18 @@ spec:
existingClaim: ganesha-export existingClaim: ganesha-export
globalMounts: globalMounts:
- path: /webdav/export - path: /webdav/export
# NAS may be off at pod start: the PVs mount with "bg", so the NFS
# mount lands on the host later. HostToContainer relays that late
# mount into the container instead of leaving an empty dir.
sda2: sda2:
type: persistentVolumeClaim type: persistentVolumeClaim
existingClaim: nas1-sda2 existingClaim: nas1-sda2
globalMounts: globalMounts:
- path: /webdav/nas1/sda2 - path: /webdav/nas1/sda2
mountPropagation: HostToContainer
sdb2: sdb2:
type: persistentVolumeClaim type: persistentVolumeClaim
existingClaim: nas1-sdb2 existingClaim: nas1-sdb2
globalMounts: globalMounts:
- path: /webdav/nas1/sdb2 - path: /webdav/nas1/sdb2
mountPropagation: HostToContainer
@@ -48,6 +48,11 @@ spec:
mountOptions: mountOptions:
- nfsvers=3 - nfsvers=3
- nolock - nolock
- soft
- bg
- retry=525600 # bg gives up after 10000 min by default; ~1 year instead
- timeo=50
- retrans=2
nfs: nfs:
server: 10.0.64.80 server: 10.0.64.80
path: /mnt/sda2 path: /mnt/sda2
@@ -85,6 +90,11 @@ spec:
mountOptions: mountOptions:
- nfsvers=3 - nfsvers=3
- nolock - nolock
- soft
- bg
- retry=525600 # bg gives up after 10000 min by default; ~1 year instead
- timeo=50
- retrans=2
nfs: nfs:
server: 10.0.64.80 server: 10.0.64.80
path: /mnt/sdb2 path: /mnt/sdb2
@@ -0,0 +1,115 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: woodpecker
namespace: ci
spec:
interval: 10m
chartRef:
kind: OCIRepository
name: woodpecker
driftDetection:
mode: enabled
values:
defaultPodOptions:
securityContext:
fsGroup: 1000 # non-root image, fresh longhorn volume is root-owned
fsGroupChangePolicy: OnRootMismatch
controllers:
server:
replicas: 1
strategy: Recreate # sqlite on RWO volume
containers:
app:
image:
repository: woodpeckerci/woodpecker-server
tag: v3.18.1
env:
WOODPECKER_HOST: https://ci.aggtaa.com
WOODPECKER_OPEN: "false"
WOODPECKER_ADMIN: anton
WOODPECKER_GITEA: "true"
WOODPECKER_GITEA_URL: https://git.aggtaa.com
WOODPECKER_DATABASE_DRIVER: sqlite3
WOODPECKER_DATABASE_DATASOURCE: /var/lib/woodpecker/woodpecker.sqlite
envFrom:
- secretRef:
name: woodpecker-secrets
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi
agent:
replicas: 1
strategy: Recreate # RWO volume for agent id
serviceAccount:
name: woodpecker-agent # created in rbac.yaml
pod:
automountServiceAccountToken: true # kubernetes backend uses in-cluster config
containers:
app:
image:
repository: woodpeckerci/woodpecker-agent
tag: v3.18.1
env:
WOODPECKER_SERVER: woodpecker:9000
WOODPECKER_BACKEND: kubernetes
WOODPECKER_BACKEND_K8S_NAMESPACE: ci
WOODPECKER_BACKEND_K8S_STORAGE_CLASS: longhorn-no-retain
WOODPECKER_BACKEND_K8S_VOLUME_SIZE: 5G
WOODPECKER_MAX_WORKFLOWS: "2"
envFrom:
- secretRef:
name: woodpecker-secrets
resources:
requests:
cpu: 20m
memory: 64Mi
limits:
memory: 256Mi
service:
server:
controller: server
ports:
http:
port: 8000
grpc:
port: 9000
ingress:
server:
className: traefik
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: ci.aggtaa.com
paths:
- path: /
pathType: Prefix
service:
identifier: server
port: http
tls:
- secretName: woodpecker-tls
hosts:
- ci.aggtaa.com
persistence:
data:
type: persistentVolumeClaim
existingClaim: woodpecker-data
advancedMounts:
server:
app:
- path: /var/lib/woodpecker
agent-config:
type: persistentVolumeClaim
existingClaim: woodpecker-agent-config
advancedMounts:
agent:
app:
- path: /etc/woodpecker
@@ -0,0 +1,10 @@
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata:
name: woodpecker
namespace: ci
spec:
interval: 10m
url: oci://ghcr.io/bjw-s-labs/helm/app-template
ref:
tag: "5.0.1"
@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrepository.yaml
- secret.enc.yaml
- pvc.yaml
- rbac.yaml
- helmrelease.yaml
+25
View File
@@ -0,0 +1,25 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: woodpecker-data
namespace: ci
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 2Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: woodpecker-agent-config
namespace: ci
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 1Gi
+33
View File
@@ -0,0 +1,33 @@
# Agent runs pipeline steps as pods in this namespace (kubernetes backend)
apiVersion: v1
kind: ServiceAccount
metadata:
name: woodpecker-agent
namespace: ci
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: woodpecker-agent
namespace: ci
rules:
- apiGroups: [""]
resources: [pods, pods/log]
verbs: [create, get, list, watch, delete]
- apiGroups: [""]
resources: [persistentvolumeclaims, services, secrets, configmaps]
verbs: [create, get, list, watch, update, delete]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: woodpecker-agent
namespace: ci
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: woodpecker-agent
subjects:
- kind: ServiceAccount
name: woodpecker-agent
namespace: ci
@@ -0,0 +1,25 @@
apiVersion: v1
kind: Secret
metadata:
name: woodpecker-secrets
namespace: ci
stringData:
WOODPECKER_AGENT_SECRET: ENC[AES256_GCM,data:UyYPJpkHb2G1Vpe2tEtkNDMktp6zFbeNtDF34Kp8vf6nAY5/2EYXBJvT4+Ns2oOGNZOupdCUmTowrJ+ITNFwhQ==,iv:EtXILwt+FFYvqvLw9kLODh0wyhrHG+GJNGZRu6wzXd4=,tag:Ly5dsTWio741VxgtPy51XQ==,type:str]
WOODPECKER_GITEA_CLIENT: ENC[AES256_GCM,data:hXBo5EwhHuCTQ4vQBAmjnLTlQ6cY2dKeJ5vZJ27wMFUsKZpS,iv:Pzt78GVSWngjVvpVSEgvsImK+siHEHOqQy+jOAxrGkc=,tag:JYu+Jf+n9J4L4LPwRPPbSw==,type:str]
WOODPECKER_GITEA_SECRET: ENC[AES256_GCM,data:nu4FTBjdJe7oHx2Vt1ElxJXVnYzONC3kKYlXwvtmVI3/DokjSGA0YwLy0jI58l7K9nBbzGKuuZM=,iv:U6mfIG+LYhSfooIK3yhJay+fQh5oCNyGUggN+MrY4os=,tag:Oho0FnhePMLhFzsyhFISZQ==,type:str]
WOODPECKER_GRPC_SECRET: ENC[AES256_GCM,data:rLiZToTcqf03gY/psoboYUsDUC16/0ZPkgOo/5ABrOvuyvHFoJPnAlvYhvSu/+qCIU1vlv7AvhfEJyEig+ULaA==,iv:q42abGbWSQb1noVhkXwiAWoIXcDUT01MTbnhaV5Plos=,tag:gioDH0cIPGoRD4auPEq71w==,type:str]
sops:
age:
- recipient: age13qwkssk8esashln2j3gfv6e9csashr4vddkdav5v38hm5wejl36qd2ew9s
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1anNnNDlnSjhMU2h4clVV
eFJnNEx6VmU1RjR0a1RQQTRML3lBaUphSkU4CnNuM0I2NkFPMlU1clFNY3V5Sk9Y
b0NJNzlUMjJsRVZMdndzUWYyNWhjeHMKLS0tIEp0Y2E3dTRwRTUyUmprRktFNUJp
TXlXWjViSGNFb1FhQ0xDT0gvajdwSlEKXB7N64RmaWgCCSvG/nNWOziO7QnN729i
jJLB6SPiHjgNMn1GP+rsAstVFStB0fn2mh6QpWrdhAM3/NFBqQfy7A==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-10-07T08:38:39Z"
mac: ENC[AES256_GCM,data:+beUA2t2YPqjsk7KXnPgJBiiQSjB7qypni669F2W3YNdTXx/ChenMxo7srjvejW3YfzPccbnBY6/TJgKCLPGm/Kf0hntejbBgYeu9hltSrIZX28efoE6tq4NicqJkPT8oD24v++H5jxRZP4epmef5t6rzJSq1xc4ETZerK0f9PU=,iv:wekT7WxL/Xm+vqCYCsZ4nvwl8VeOugURNvbHs/KQhRk=,tag:gH5efL/YUtJEewcY0G3Ctw==,type:str]
encrypted_regex: ^(data|stringData)$
version: 3.11.0
@@ -38,7 +38,7 @@ spec:
zigbee2mqtt: zigbee2mqtt:
homeassistant: homeassistant:
enabled: true enabled: true
legacy_entity_attributes: true experimental_event_entities: true
permit_join: false permit_join: false
timezone: Europe/Moscow timezone: Europe/Moscow
@@ -75,7 +75,7 @@ spec:
ingressClassName: traefik ingressClassName: traefik
annotations: annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.middlewares: auth-authelia@kubernetescrd traefik.ingress.kubernetes.io/router.middlewares: auth-tinyauth@kubernetescrd
hosts: hosts:
- host: zigbee2mqtt.aggtaa.com - host: zigbee2mqtt.aggtaa.com
paths: paths:
@@ -24,6 +24,28 @@ spec:
upgrade: upgrade:
remediation: remediation:
retries: 3 retries: 3
postRenderers:
- kustomize:
patches:
- target:
kind: DaemonSet
name: longhorn-manager
# chart hardcodes readinessProbe without timeoutSeconds, so it falls back to the
# k8s default of 1s; under CPU contention (e.g. busy nodes) that's too tight and
# causes spurious "Readiness probe failed ... Client.Timeout exceeded" flapping
patch: |
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: longhorn-manager
spec:
template:
spec:
containers:
- name: longhorn-manager
readinessProbe:
timeoutSeconds: 5
failureThreshold: 5
values: values:
defaultSettings: defaultSettings:
defaultReplicaCount: 3 # по числу нод control-plane defaultReplicaCount: 3 # по числу нод control-plane
@@ -39,10 +61,9 @@ spec:
# чтобы не разъезжались реплики при 3 нодах # чтобы не разъезжались реплики при 3 нодах
replicaSoftAntiAffinity: false replicaSoftAntiAffinity: false
persistence: persistence:
defaultClass: true # the default "longhorn" StorageClass lives in ../storage-classes/longhorn.yaml -
defaultClassReplicaCount: 3 # the chart hardcodes staleReplicaTimeout: "30" and offers no value to override it
defaultFsType: ext4 createStorageClass: false
reclaimPolicy: Retain
csi: csi:
attacherReplicaCount: 3 attacherReplicaCount: 3
provisionerReplicaCount: 3 provisionerReplicaCount: 3
@@ -5,7 +5,7 @@ metadata:
namespace: longhorn-system namespace: longhorn-system
annotations: annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.middlewares: auth-authelia@kubernetescrd traefik.ingress.kubernetes.io/router.middlewares: auth-tinyauth@kubernetescrd
spec: spec:
ingressClassName: traefik ingressClassName: traefik
rules: rules:
@@ -1,5 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1 apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
resources: resources:
- longhorn.yaml
- longhorn-no-retain.yaml - longhorn-no-retain.yaml
- longhorn-single-replica.yaml - longhorn-single-replica.yaml
@@ -4,9 +4,11 @@ metadata:
name: longhorn-no-retain name: longhorn-no-retain
annotations: annotations:
storageclass.kubernetes.io/is-default-class: "false" storageclass.kubernetes.io/is-default-class: "false"
# StorageClass parameters are immutable - let Flux recreate the object on change
kustomize.toolkit.fluxcd.io/force: enabled
provisioner: driver.longhorn.io provisioner: driver.longhorn.io
reclaimPolicy: Delete reclaimPolicy: Delete
allowVolumeExpansion: true allowVolumeExpansion: true
parameters: parameters:
numberOfReplicas: "3" numberOfReplicas: "3"
staleReplicaTimeout: "30" staleReplicaTimeout: "2880"
@@ -4,9 +4,11 @@ metadata:
name: longhorn-single-replica name: longhorn-single-replica
annotations: annotations:
storageclass.kubernetes.io/is-default-class: "false" storageclass.kubernetes.io/is-default-class: "false"
# StorageClass parameters are immutable - let Flux recreate the object on change
kustomize.toolkit.fluxcd.io/force: enabled
provisioner: driver.longhorn.io provisioner: driver.longhorn.io
reclaimPolicy: Retain reclaimPolicy: Retain
allowVolumeExpansion: true allowVolumeExpansion: true
parameters: parameters:
numberOfReplicas: "1" numberOfReplicas: "1"
staleReplicaTimeout: "30" staleReplicaTimeout: "2880"
@@ -0,0 +1,27 @@
# Default Longhorn class. Managed here instead of by the chart (persistence.createStorageClass: false
# in the HelmRelease) because the chart hardcodes staleReplicaTimeout: "30".
# Parameters otherwise mirror what chart 1.12.1 rendered.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: longhorn
annotations:
storageclass.kubernetes.io/is-default-class: "true"
# StorageClass parameters are immutable - let Flux recreate the object on change
kustomize.toolkit.fluxcd.io/force: enabled
provisioner: driver.longhorn.io
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: Immediate
parameters:
numberOfReplicas: "3"
# minutes a failed replica is kept for reuse before it is discarded and fully rebuilt;
# 30 was shorter than a planned node shutdown and caused a ~1.2TB full rebuild of kube-3
staleReplicaTimeout: "2880"
fromBackup: ""
fsType: ext4
dataLocality: disabled
unmapMarkSnapChainRemoved: ignored
disableRevisionCounter: "true"
dataEngine: v1
backupTargetName: default
+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: ci
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: dev
+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: notify
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: tools
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: video
+18
View File
@@ -0,0 +1,18 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: dev
namespace: flux-system
spec:
interval: 10m
sourceRef:
kind: GitRepository
name: flux-system
path: ./clusters/base/apps/dev
prune: true
wait: true
dependsOn:
- name: namespaces
- name: infra
- name: auth
retryInterval: 5s
+17
View File
@@ -0,0 +1,17 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: it-tools
namespace: flux-system
spec:
interval: 10m
sourceRef:
kind: GitRepository
name: flux-system
path: ./clusters/base/apps/it-tools
prune: true
wait: true
dependsOn:
- name: namespaces
- name: infra
retryInterval: 5s
+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
+7
View File
@@ -16,3 +16,10 @@ resources:
- immich.yaml - immich.yaml
- joplin.yaml - joplin.yaml
- gitea.yaml - gitea.yaml
- dev.yaml
- woodpecker.yaml
- tinyauth.yaml
- it-tools.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
+21
View File
@@ -0,0 +1,21 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: ntfy
namespace: flux-system
spec:
interval: 10m
sourceRef:
kind: GitRepository
name: flux-system
path: ./clusters/base/apps/ntfy
prune: true
wait: true
dependsOn:
- name: namespaces
- name: infra
retryInterval: 5s
decryption:
provider: sops
secretRef:
name: sops-age-ntfy
+21
View File
@@ -0,0 +1,21 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: tinyauth
namespace: flux-system
spec:
interval: 10m
sourceRef:
kind: GitRepository
name: flux-system
path: ./clusters/base/apps/tinyauth
prune: true
wait: true
dependsOn:
- name: namespaces
- name: infra
retryInterval: 5s
decryption:
provider: sops
secretRef:
name: sops-age-tinyauth
+22
View File
@@ -0,0 +1,22 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: woodpecker
namespace: flux-system
spec:
interval: 10m
sourceRef:
kind: GitRepository
name: flux-system
path: ./clusters/base/apps/woodpecker
prune: true
wait: true
dependsOn:
- name: namespaces
- name: infra
- name: gitea
retryInterval: 5s
decryption:
provider: sops
secretRef:
name: sops-age-woodpecker
+1 -1
View File
@@ -11,7 +11,7 @@ spec:
branch: prod branch: prod
secretRef: secretRef:
name: flux-system name: flux-system
url: http://10.0.64.157:5000/anton/kube url: http://gitea-app.git.svc.cluster.local:3000/anton/kube
--- ---
apiVersion: kustomize.toolkit.fluxcd.io/v1 apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization kind: Kustomization
@@ -17,3 +17,9 @@ resources:
- ../../base/namespaces/syncthing.yaml - ../../base/namespaces/syncthing.yaml
- ../../base/namespaces/photo.yaml - ../../base/namespaces/photo.yaml
- ../../base/namespaces/git.yaml - ../../base/namespaces/git.yaml
- ../../base/namespaces/dev.yaml
- ../../base/namespaces/ci.yaml
- ../../base/namespaces/tools.yaml
- ../../base/namespaces/notify.yaml
- ../../base/namespaces/iot.yaml
- ../../base/namespaces/video.yaml