Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33d92e133c | ||
|
|
150a83ee38 | ||
|
|
dd6553ab07 | ||
|
|
0b845e7c76 | ||
|
|
685905b1df | ||
|
|
f9848d2bf5 | ||
|
|
2e62216bc7 | ||
|
|
fac46aa7ac | ||
|
|
6fb760edae | ||
|
|
96fcb426aa | ||
|
|
56a9859e50 | ||
|
|
4df1018e05 | ||
|
|
0e4d516722 | ||
|
|
603db4ed95 | ||
|
|
5b03d6b7af | ||
|
|
343e4845e2 | ||
|
|
5d56c35017 |
@@ -217,3 +217,32 @@ steps:
|
||||
--dockerfile Containerfile \
|
||||
--destination gitea.renner.uno/gitea_admin/alpine-openssl:$TAG \
|
||||
--destination gitea.renner.uno/gitea_admin/alpine-openssl:latest
|
||||
|
||||
# Heating CAN readers: open3e (Vitocal/VX3) + E3onCAN (E380 meter), one image
|
||||
# run as two services on edge-1. Replaces pip-into-venv on the host, where an
|
||||
# unpinned VCS requirement re-cloned and restarted the heating bridge on every
|
||||
# Ansible converge. Not a ci-* image -- it is a runtime artifact -- but it
|
||||
# builds the same way and lands in the same registry so the audit sees it.
|
||||
- name: build-heating-can
|
||||
image: gcr.io/kaniko-project/executor:v1.23.2-debug
|
||||
directory: ci/images/heating-can
|
||||
depends_on: []
|
||||
when:
|
||||
- event: manual
|
||||
- event: push
|
||||
path: ["ci/images/heating-can/**", ".woodpecker/build-ci-images.yml"]
|
||||
environment:
|
||||
GITEA_USER: gitea_admin
|
||||
GITEA_TOKEN:
|
||||
from_secret: ci_images_token
|
||||
commands:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')
|
||||
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"}}}' "$AUTH" > /kaniko/.docker/config.json
|
||||
- TAG=$(date +%Y-%m-%d)
|
||||
- |
|
||||
/kaniko/executor \
|
||||
--context . \
|
||||
--dockerfile Containerfile \
|
||||
--destination gitea.renner.uno/gitea_admin/heating-can:$TAG \
|
||||
--destination gitea.renner.uno/gitea_admin/heating-can:latest
|
||||
|
||||
@@ -39,6 +39,8 @@ Build and boot the dedicated installer image — see
|
||||
(inventory, plays, this file) and its address is never recorded. Do not
|
||||
pin it to a reservation.
|
||||
4. Add the host to `[edge_host]` in `inventory/pve_hosts.yml`, by name.
|
||||
That group is the base: every edge thin client belongs to it, and it
|
||||
assumes no peripherals.
|
||||
5. From the workstation:
|
||||
```
|
||||
ansible-playbook -i inventory/pve_hosts.yml plays/install-edge-host.yml
|
||||
@@ -49,6 +51,26 @@ Build and boot the dedicated installer image — see
|
||||
the point: a run that touches only the new box tells you nothing about
|
||||
the others.
|
||||
|
||||
## Peripherals
|
||||
|
||||
Each attached device is its own inventory group and its own playbook. Group
|
||||
membership states which hardware a host physically has -- there are no flags
|
||||
and no `when:` on the roles, so moving a stick between boxes is a one-line
|
||||
inventory change.
|
||||
|
||||
| device | group | playbook |
|
||||
|---|---|---|
|
||||
| Innomaker USB2CAN (Vitocal/VX3 + E380) | `heating_can` | `plays/install-heating-can.yml` |
|
||||
| Nabu Casa ZWA-2 Z-Wave radio | `zwave_gateway` | `plays/install-zwave-gateway.yml` |
|
||||
| FTDI serial power-meter head | `power_meter` | `plays/install-power-meter.yml` |
|
||||
|
||||
Add the host to the matching group, then run that playbook. They depend on
|
||||
the base play having run: all three need the local MQTT bridge, and two of
|
||||
them need docker.
|
||||
|
||||
A host with nothing attached is simply absent from all three groups and needs
|
||||
none of these commands.
|
||||
|
||||
## Secrets (before first play run)
|
||||
|
||||
Create `inventory/group_vars/edge_host/vault.yml` with ansible-vault; the
|
||||
@@ -121,8 +143,12 @@ Afterwards:
|
||||
deliberate for sml2mqtt, acceptable for zwave-js). If a zwave-js bump
|
||||
changes its Alpine major, also update `edge_zwave_alpine_base` and refresh
|
||||
the preloaded `.so` (see "Logging & eMMC" → zwave-js).
|
||||
- open3e updates: re-run the play; the pip task tracks the git default
|
||||
branch (pin `edge_open3e_pkg` to a tag if that ever bites).
|
||||
- open3e / E3onCAN updates: both are pinned to commits in
|
||||
`roles/heating-can/defaults/main.yml`, so re-running the play does NOT pick
|
||||
up upstream changes -- that is deliberate, these drive the heat pump. The
|
||||
weekly update-audit reports when either falls behind (see
|
||||
`ci/agents/update-audit/prompt.md`); bump the pin and re-run.
|
||||
Pinning to a tag is not an option for open3e: it publishes none.
|
||||
- Ad-hoc CAN scans: `systemctl stop open3e` first (single-instance rule),
|
||||
restart afterwards.
|
||||
- can-utils (`candump`) is AUR-only on Arch — install manually via
|
||||
|
||||
@@ -36,3 +36,18 @@ keepalived_ntp_sysctl:
|
||||
|
||||
# Realtek NIC on the Wyse 3040 has no hardware timestamping.
|
||||
chrony_hwtimestamp: false
|
||||
|
||||
# --- Local MQTT contract -------------------------------------------------
|
||||
# Shared between the broker and its clients, so it belongs to the inventory
|
||||
# rather than to either side's role. mqtt-bridge listens here; heating-can and
|
||||
# power-meter-sml publish here; and the bridge authenticates to the cluster
|
||||
# broker AS the open3e user, so it needs that identity too.
|
||||
#
|
||||
# Keeping these in one role's defaults only worked while a single play loaded
|
||||
# every role at once. Splitting the plays by capability exposed it: the bridge
|
||||
# could not render without a heating-can default, and the CAN services could
|
||||
# not render without an mqtt-bridge default.
|
||||
edge_mqtt_host: 127.0.0.1
|
||||
edge_mqtt_port: 1883
|
||||
edge_open3e_mqtt_user: open3e
|
||||
edge_open3e_mqtt_topic: open3e
|
||||
|
||||
@@ -41,9 +41,36 @@ nas-1.renner.uno ansible_user=ladmin ansible_python_interpreter=/usr/bin/python3
|
||||
[grafana]
|
||||
grafana.srv.renner.uno ansible_user=ladmin
|
||||
|
||||
# Every bare-metal edge thin client. Gets the host-generic roles only:
|
||||
# flash-wear handling, the log shipper, the MQTT bridge, the VLAN
|
||||
# sub-interface. Nothing here assumes any peripheral is attached.
|
||||
[edge_host]
|
||||
edge-1.iot.renner.uno ansible_user=root
|
||||
|
||||
# Capability groups. Membership states which hardware a host physically has,
|
||||
# and each service playbook targets the group for its own concern. No flags and
|
||||
# no `when:` on the roles -- moving a stick to another box is a one-line change
|
||||
# here, which is what it should be.
|
||||
#
|
||||
# These are children of [edge_host], declared below: a peripheral is added to an
|
||||
# edge client, it does not replace the base. Making that a :children relation
|
||||
# rather than a comment means a host added only to a capability group still
|
||||
# resolves group_vars/edge_host -- where the local MQTT contract lives -- so it
|
||||
# cannot silently come up without edge_mqtt_host and friends.
|
||||
[zwave_gateway]
|
||||
edge-1.iot.renner.uno
|
||||
|
||||
[heating_can]
|
||||
edge-1.iot.renner.uno
|
||||
|
||||
[power_meter]
|
||||
edge-1.iot.renner.uno
|
||||
|
||||
[edge_host:children]
|
||||
zwave_gateway
|
||||
heating_can
|
||||
power_meter
|
||||
|
||||
# NTP servers. Includes edge-1 so the role survives the PVE hosts being
|
||||
# retired for bare-metal Talos, which cannot run chrony or keepalived itself.
|
||||
# See plays/install-ntp-ha.yml for why keepalived is scoped more narrowly
|
||||
|
||||
@@ -3,10 +3,18 @@
|
||||
# hardware-bound services, because Talos supports neither SocketCAN nor USB
|
||||
# passthrough.
|
||||
#
|
||||
# One role per concern. Previously all of this lived in a single `edge-host`
|
||||
# role, which meant a host could not take the flash-wear handling or the log
|
||||
# shipper without also being handed the Z-Wave radio and the CAN bus. That is
|
||||
# what blocked a second thin client from joining as an NTP peer.
|
||||
# This play is the BASE only: what every edge thin client gets regardless of
|
||||
# what is plugged into it. The peripherals live in their own playbooks against
|
||||
# their own inventory groups:
|
||||
#
|
||||
# plays/install-zwave-gateway.yml [zwave_gateway] ZWA-2 radio
|
||||
# plays/install-heating-can.yml [heating_can] Innomaker USB2CAN
|
||||
# plays/install-power-meter.yml [power_meter] FTDI meter head
|
||||
#
|
||||
# Splitting the roles without splitting the plays would have kept the coupling:
|
||||
# one play applying every role to one group is the monolith again, a layer up.
|
||||
# A host with no peripherals is simply absent from those groups -- no flags, no
|
||||
# `when:` on the roles.
|
||||
#
|
||||
# Self-contained: `certificates` and `common` are the parts of plays/common.yml
|
||||
# that apply to an edge host, carried here so bootstrapping one is a single
|
||||
@@ -14,76 +22,20 @@
|
||||
# metal, not VMs) and monitoring-agent (Debian-oriented; these hosts ship logs
|
||||
# via edge-log-shipper).
|
||||
#
|
||||
# Order is dependency order, not alphabetical:
|
||||
# mqtt-bridge before its clients, so they have somewhere to publish
|
||||
# edge-flash-wear before docker, so the journal is volatile before
|
||||
# containers start logging into it
|
||||
# socketcan before heating-can, which consumes can0
|
||||
- name: Install Arch edge host
|
||||
# Order is dependency order: edge-flash-wear before docker, so the journal is
|
||||
# volatile before containers start logging into it.
|
||||
- name: Install Arch edge host (base)
|
||||
hosts: edge_host
|
||||
# Connection is already root (ansible_user=root), so this is a root-to-root
|
||||
# no-op today. Kept explicit, matching plays/common.yml, so the roles below
|
||||
# do not silently depend on the connection user staying privileged.
|
||||
become: true
|
||||
|
||||
# Defined on the play, not taken from a role's defaults: these are used by
|
||||
# pre_tasks, which must not depend on role default resolution.
|
||||
#
|
||||
# heating-can, zwave-gateway and power-meter-sml each declare edge_base_dir
|
||||
# in their own defaults too, so they stay usable standalone. Play vars win,
|
||||
# so these are the effective values -- keep them in step if the path moves.
|
||||
vars:
|
||||
edge_base_dir: /opt/edge-host
|
||||
edge_legacy_compose: "{{ edge_base_dir }}/docker-compose.yml"
|
||||
|
||||
# One-time migration off the shared compose project, BEFORE any role tries
|
||||
# to start a container.
|
||||
#
|
||||
# zwave-js and sml2mqtt used to live in one project at edge_legacy_compose,
|
||||
# both with a fixed container_name. Docker enforces container names globally,
|
||||
# not per project, so while the old project still owns those names the new
|
||||
# per-service projects cannot create them -- `docker compose up -d` fails with
|
||||
# "Conflict: container name already in use", which is not the missing-USB-device
|
||||
# error the roles tolerate, so the play would abort.
|
||||
#
|
||||
# It belongs here rather than in either role: it is a migration step for both
|
||||
# of them, and putting it in one meant whichever role ran first hit the
|
||||
# conflict. Guarded on the legacy file, so it runs once and then never again.
|
||||
#
|
||||
# check mode cannot verify this: command tasks are skipped, not simulated, so
|
||||
# a --check run neither hits the conflict nor proves the teardown works.
|
||||
pre_tasks:
|
||||
- name: Retire the legacy shared compose project
|
||||
ansible.builtin.command:
|
||||
cmd: docker compose -f "{{ edge_legacy_compose }}" down
|
||||
args:
|
||||
removes: "{{ edge_legacy_compose }}"
|
||||
failed_when: false
|
||||
|
||||
- name: Remove the legacy shared compose file
|
||||
ansible.builtin.file:
|
||||
path: "{{ edge_legacy_compose }}"
|
||||
state: absent
|
||||
|
||||
- name: Remove the legacy zwave-js env from the shared project root
|
||||
ansible.builtin.file:
|
||||
path: "{{ edge_base_dir }}/zwavejs.env"
|
||||
state: absent
|
||||
|
||||
roles:
|
||||
- certificates
|
||||
- common
|
||||
|
||||
# Host-generic. An edge host with no attached peripherals takes these and
|
||||
# nothing below them.
|
||||
- edge-flash-wear
|
||||
- docker-install
|
||||
- edge-log-shipper
|
||||
- mqtt-bridge
|
||||
- edge-vlan
|
||||
|
||||
# Hardware-bound. Each needs its own device present.
|
||||
- socketcan
|
||||
- heating-can
|
||||
- zwave-gateway
|
||||
- power-meter-sml
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
# Heating system over the Viessmann E3 CAN bus, for hosts with the Innomaker
|
||||
# USB2CAN adapter attached.
|
||||
#
|
||||
# Targets [heating_can]: membership means the adapter is physically present.
|
||||
# Two roles because they are two concerns -- the interface is host network
|
||||
# state, the services consume it -- but one group, because both need the same
|
||||
# hardware.
|
||||
#
|
||||
# Requires the base play (plays/install-edge-host.yml) to have run: these
|
||||
# services publish to the local MQTT bridge it sets up.
|
||||
- name: Install heating CAN services
|
||||
hosts: heating_can
|
||||
become: true
|
||||
|
||||
roles:
|
||||
- socketcan
|
||||
- heating-can
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
# Power-meter reader (sml2mqtt) for hosts with the FTDI serial meter head
|
||||
# attached.
|
||||
#
|
||||
# Targets [power_meter]: membership means the meter head is physically present.
|
||||
#
|
||||
# Requires the base play (plays/install-edge-host.yml) to have run: it needs
|
||||
# docker and publishes to the local MQTT bridge.
|
||||
- name: Install power-meter reader
|
||||
hosts: power_meter
|
||||
become: true
|
||||
|
||||
roles:
|
||||
- power-meter-sml
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
# Z-Wave gateway (zwave-js-ui) for hosts with the ZWA-2 USB radio attached.
|
||||
#
|
||||
# Targets [zwave_gateway]: membership means the radio is physically present.
|
||||
# Moving the stick to another edge host is a one-line inventory change.
|
||||
#
|
||||
# Requires the base play (plays/install-edge-host.yml) to have run: it needs
|
||||
# docker, and the flash-wear handling that the libeatmydata preload exists to
|
||||
# complement.
|
||||
- name: Install Z-Wave gateway
|
||||
hosts: zwave_gateway
|
||||
become: true
|
||||
|
||||
roles:
|
||||
- zwave-gateway
|
||||
@@ -6,7 +6,6 @@
|
||||
# Service root. Kept per-role so the role is usable on its own.
|
||||
edge_base_dir: /opt/edge-host
|
||||
|
||||
edge_open3e_pkg: git+https://github.com/open3e/open3e.git
|
||||
edge_open3e_mqtt_user: open3e
|
||||
edge_open3e_mqtt_topic: open3e
|
||||
edge_open3e_mqtt_format: "{device}_{didNumber:04d}_{didName}"
|
||||
@@ -37,16 +36,25 @@ edge_open3e_devices:
|
||||
vitocal: { tx: "0x680", dpList: "vcal" }
|
||||
# dpList als Pfad: Overlay-Datei mit den Batteriemodul-DIDs (siehe
|
||||
# files/open3e-vx3plus.py) — die Stock-vx3-Liste filtert sie sonst weg.
|
||||
vx3: { tx: "0x6a1", dpList: "/opt/edge-host/open3e/vx3plus.py" }
|
||||
vx3: { tx: "0x6a1", dpList: "/config/vx3plus.py" }
|
||||
vcmu: { tx: "0x68c" }
|
||||
|
||||
# --- E3onCAN: passive E380 energy-meter listener (no bus writes, so it
|
||||
# coexists with the single open3e instance). The meter broadcasts at
|
||||
# ~5 Hz; the gap throttles MQTT to one update per DID per interval.
|
||||
edge_e3oncan_repo: https://github.com/MyHomeMyData/E3onCAN.git
|
||||
edge_e3oncan_version: 461dcbdf97e8d72d2edb9929c63b39382af6c3a1 # V0.5.2
|
||||
edge_e3oncan_gap_seconds: 10
|
||||
edge_e3oncan_mqtt_format: "{device}_{didNumber:04d}_{didName}"
|
||||
|
||||
# Secrets come from inventory/group_vars/edge_host/vault.yml:
|
||||
# edge_open3e_mqtt_password
|
||||
|
||||
# The image carrying both readers, built and pinned in CI
|
||||
# (ci/images/heating-can/Containerfile). The source pins live in the
|
||||
# Containerfile, in the artifact -- not here -- so what is deployed is a tag,
|
||||
# and `docker compose up -d` on an unchanged tag is a genuine no-op.
|
||||
#
|
||||
# Tracked by the weekly update-audit as an ordinary image, which is why this is
|
||||
# simpler to keep current than the two git refs it replaced: one publishes no
|
||||
# tags at all, the other was pinned to an untagged commit between releases.
|
||||
heating_can_image: gitea.renner.uno/gitea_admin/heating-can:latest
|
||||
|
||||
# Compose project directory. Separate from the old native layout under
|
||||
# {{ edge_base_dir }}/open3e, which this role removes.
|
||||
heating_can_base_dir: "{{ edge_base_dir }}/heating-can"
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
---
|
||||
- name: Restart open3e
|
||||
- name: Reload systemd
|
||||
ansible.builtin.systemd:
|
||||
name: open3e
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
|
||||
- name: Restart e3oncan
|
||||
ansible.builtin.systemd:
|
||||
name: e3oncan
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
- name: Restart heating-can
|
||||
ansible.builtin.command:
|
||||
cmd: docker compose up -d --force-recreate
|
||||
chdir: "{{ heating_can_base_dir }}"
|
||||
changed_when: true
|
||||
|
||||
@@ -1,116 +1,119 @@
|
||||
---
|
||||
# Heating system over the Viessmann E3 CAN bus.
|
||||
#
|
||||
# Two readers of the same heating installation:
|
||||
# Two readers of the same heating installation, supervised inside one container
|
||||
# from one image (ci/images/heating-can):
|
||||
# open3e UDS request/response to the Vitocal heat pump and Vitocharge VX3
|
||||
# e3oncan passive CANopen listener for the E380 grid energy meter, which
|
||||
# feeds the heat pump s own energy management
|
||||
# feeds the heat pump's own energy management
|
||||
#
|
||||
# They coexist because e3oncan issues no bus writes. Both are native installs
|
||||
# (venv + systemd) rather than containers: they need a SocketCAN interface,
|
||||
# which is host network state. CAN_RAW sockets need no capabilities, so they
|
||||
# run as unprivileged system users.
|
||||
# They coexist because e3oncan issues no bus writes.
|
||||
#
|
||||
# The can0 interface itself belongs to the socketcan role -- an interface is
|
||||
# not the same concern as the services that consume it -- so this role must
|
||||
# run after it.
|
||||
# Nothing is installed here. The image is built in CI with both sources pinned,
|
||||
# published to the registry, and this host only pulls and runs it. That is what
|
||||
# removes the churn: these were pip-installed into venvs on the node, and pip
|
||||
# re-cloned and rebuilt a VCS requirement on every converge -- restarting the
|
||||
# heating bridge each time -- which pinning the SHA did not fix.
|
||||
#
|
||||
# network_mode: host is required, not incidental: SocketCAN interfaces live in
|
||||
# the host network namespace, so can0 is only reachable that way. It is also how
|
||||
# the local MQTT broker on 127.0.0.1 is reached. CAN_RAW needs no capabilities,
|
||||
# so both run unprivileged.
|
||||
#
|
||||
# The can0 interface itself belongs to the socketcan role; this role consumes it.
|
||||
|
||||
- name: Install build/runtime packages for the CAN services
|
||||
ansible.builtin.package:
|
||||
name:
|
||||
- python
|
||||
- python-packaging
|
||||
- git
|
||||
state: present
|
||||
|
||||
- name: Create open3e system user
|
||||
ansible.builtin.user:
|
||||
name: open3e
|
||||
system: true
|
||||
create_home: false
|
||||
shell: /usr/bin/nologin
|
||||
|
||||
# Owned by the service user: udsoncan writes its log file into the
|
||||
# working directory.
|
||||
- name: Create open3e directory
|
||||
- name: Create heating-can directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ edge_base_dir }}/open3e"
|
||||
path: "{{ heating_can_base_dir }}"
|
||||
state: directory
|
||||
owner: open3e
|
||||
group: open3e
|
||||
mode: "0755"
|
||||
|
||||
- name: Install open3e into a venv
|
||||
ansible.builtin.pip:
|
||||
name: "{{ edge_open3e_pkg }}"
|
||||
virtualenv: "{{ edge_base_dir }}/open3e/venv"
|
||||
virtualenv_command: python -m venv
|
||||
notify: Restart open3e
|
||||
# --- Migration off the native install ---------------------------------------
|
||||
# Must happen before the containers start. Two open3e instances issuing UDS
|
||||
# requests on one CAN bus is the failure mode to avoid, so the old units are
|
||||
# stopped and masked rather than merely disabled -- masked so a stray
|
||||
# `systemctl start` or a leftover dependency cannot bring them back alongside
|
||||
# the containers.
|
||||
- name: Stop and mask the native CAN services
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ item }}"
|
||||
state: stopped
|
||||
enabled: false
|
||||
masked: true
|
||||
loop:
|
||||
- open3e
|
||||
- e3oncan
|
||||
failed_when: false
|
||||
|
||||
- name: Remove the native unit files
|
||||
ansible.builtin.file:
|
||||
path: "/etc/systemd/system/{{ item }}.service"
|
||||
state: absent
|
||||
loop:
|
||||
- open3e
|
||||
- e3oncan
|
||||
notify: Reload systemd
|
||||
|
||||
# The venvs are the point of this change -- leaving them would leave the thing
|
||||
# that churned. Their configuration is NOT removed: devices.json, the overlay
|
||||
# and the env file are still rendered below and bind-mounted into the
|
||||
# containers.
|
||||
- name: Remove the native venvs
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- "{{ edge_base_dir }}/open3e/venv"
|
||||
- "{{ edge_base_dir }}/e3oncan/venv"
|
||||
- "{{ edge_base_dir }}/e3oncan/src"
|
||||
- "{{ edge_base_dir }}/open3e"
|
||||
- "{{ edge_base_dir }}/e3oncan"
|
||||
|
||||
# --- Configuration (bind-mounted into the containers) ------------------------
|
||||
- name: Deploy open3e devices.json
|
||||
ansible.builtin.copy:
|
||||
content: "{{ edge_open3e_devices | to_nice_json }}\n"
|
||||
dest: "{{ heating_can_base_dir }}/devices.json"
|
||||
mode: "0644"
|
||||
notify: Restart heating-can
|
||||
|
||||
- name: Deploy open3e vx3 datapoint overlay (battery-module DIDs)
|
||||
ansible.builtin.copy:
|
||||
src: open3e-vx3plus.py
|
||||
dest: "{{ edge_base_dir }}/open3e/vx3plus.py"
|
||||
dest: "{{ heating_can_base_dir }}/vx3plus.py"
|
||||
mode: "0644"
|
||||
notify: Restart open3e
|
||||
notify: Restart heating-can
|
||||
|
||||
- name: Deploy open3e devices.json
|
||||
ansible.builtin.copy:
|
||||
content: "{{ edge_open3e_devices | to_nice_json }}\n"
|
||||
dest: "{{ edge_base_dir }}/open3e/devices.json"
|
||||
mode: "0644"
|
||||
notify: Restart open3e
|
||||
|
||||
- name: Deploy open3e environment (broker credential)
|
||||
# --- Compose project ---------------------------------------------------------
|
||||
- name: Deploy the broker credential env file
|
||||
ansible.builtin.template:
|
||||
src: open3e.env.j2
|
||||
dest: "{{ edge_base_dir }}/open3e/open3e.env"
|
||||
owner: open3e
|
||||
src: heating-can.env.j2
|
||||
dest: "{{ heating_can_base_dir }}/heating-can.env"
|
||||
mode: "0600"
|
||||
notify: Restart open3e
|
||||
notify: Restart heating-can
|
||||
|
||||
- name: Deploy open3e systemd unit
|
||||
- name: Deploy heating-can compose project
|
||||
ansible.builtin.template:
|
||||
src: open3e.service.j2
|
||||
dest: /etc/systemd/system/open3e.service
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ heating_can_base_dir }}/docker-compose.yml"
|
||||
mode: "0644"
|
||||
notify: Restart open3e
|
||||
notify: Restart heating-can
|
||||
|
||||
- name: Enable open3e
|
||||
ansible.builtin.systemd:
|
||||
name: open3e
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
- name: Pull the heating-can image
|
||||
ansible.builtin.command:
|
||||
cmd: docker compose pull
|
||||
chdir: "{{ heating_can_base_dir }}"
|
||||
changed_when: false
|
||||
|
||||
|
||||
# --- E3onCAN: direct E380 meter reads (passive; per-review decision not
|
||||
# to trust the VX3's relayed meter values). Same broker user as open3e;
|
||||
# topics land under the same open3e/ prefix as e380_<did>_<name>.
|
||||
- name: Check out E3onCAN (pinned)
|
||||
ansible.builtin.git:
|
||||
repo: "{{ edge_e3oncan_repo }}"
|
||||
dest: "{{ edge_base_dir }}/e3oncan/src"
|
||||
version: "{{ edge_e3oncan_version }}"
|
||||
notify: Restart e3oncan
|
||||
|
||||
- name: Create E3onCAN venv with requirements
|
||||
ansible.builtin.pip:
|
||||
requirements: "{{ edge_base_dir }}/e3oncan/src/requirements.txt"
|
||||
virtualenv: "{{ edge_base_dir }}/e3oncan/venv"
|
||||
virtualenv_command: python -m venv
|
||||
notify: Restart e3oncan
|
||||
|
||||
- name: Deploy e3oncan systemd unit
|
||||
ansible.builtin.template:
|
||||
src: e3oncan.service.j2
|
||||
dest: /etc/systemd/system/e3oncan.service
|
||||
mode: "0644"
|
||||
notify: Restart e3oncan
|
||||
|
||||
- name: Enable e3oncan
|
||||
ansible.builtin.systemd:
|
||||
name: e3oncan
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
# Unlike the zwave and meter services there is no missing-device tolerance here:
|
||||
# can0 is created by the socketcan role from a USB adapter that is part of this
|
||||
# host's purpose. If it is absent the heating telemetry is down and the run
|
||||
# should say so rather than pass quietly.
|
||||
- name: Start heating-can services
|
||||
ansible.builtin.command:
|
||||
cmd: docker compose up -d
|
||||
chdir: "{{ heating_can_base_dir }}"
|
||||
register: heating_can_up
|
||||
changed_when: >-
|
||||
'Started' in heating_can_up.stderr or 'Created' in heating_can_up.stderr
|
||||
or 'Started' in heating_can_up.stdout or 'Creating' in heating_can_up.stdout
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Managed by ansible (roles/heating-can).
|
||||
#
|
||||
# One container. Both CAN readers live in the image and are supervised inside
|
||||
# it -- this file supplies configuration and nothing else. It does not know
|
||||
# open3e's or E3onCAN's command line.
|
||||
#
|
||||
# network_mode: host is required: SocketCAN interfaces live in the host network
|
||||
# namespace, so {{ edge_can_interface }} is only reachable this way, and it is
|
||||
# also how the local MQTT broker is reached. CAN_RAW needs no capabilities, so
|
||||
# the image runs unprivileged.
|
||||
services:
|
||||
heating-can:
|
||||
image: {{ heating_can_image }}
|
||||
container_name: heating-can
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./devices.json:/config/devices.json:ro
|
||||
- ./vx3plus.py:/config/vx3plus.py:ro
|
||||
env_file:
|
||||
# Broker credential, 0600 -- see heating-can.env.j2.
|
||||
- heating-can.env
|
||||
environment:
|
||||
CAN_INTERFACE: "{{ edge_can_interface }}"
|
||||
MQTT_HOST: "{{ edge_mqtt_host }}"
|
||||
MQTT_PORT: "{{ edge_mqtt_port }}"
|
||||
MQTT_TOPIC: "{{ edge_open3e_mqtt_topic }}"
|
||||
MQTT_FORMAT: "{{ edge_open3e_mqtt_format }}"
|
||||
E3ONCAN_MQTT_FORMAT: "{{ edge_e3oncan_mqtt_format }}"
|
||||
OPEN3E_CONFIG: /config/devices.json
|
||||
OPEN3E_CLIENT_ID: open3e-edge
|
||||
OPEN3E_POLL_SECONDS: "{{ edge_open3e_poll_seconds }}"
|
||||
OPEN3E_DATAPOINTS: "{{ edge_open3e_datapoints }}"
|
||||
E3ONCAN_DEVICE: e380
|
||||
E3ONCAN_GAP_SECONDS: "{{ edge_e3oncan_gap_seconds }}"
|
||||
@@ -1,32 +0,0 @@
|
||||
# Managed by ansible (roles/heating-can).
|
||||
# PASSIVE listener for the E380 energy meter's CANopen broadcasts — no
|
||||
# writes on the bus, so it may run alongside the (single) open3e
|
||||
# instance. Reads the meter directly instead of trusting the VX3's
|
||||
# relayed values.
|
||||
[Unit]
|
||||
Description=E3onCAN E380 energy-meter CAN listener (read-only)
|
||||
BindsTo=sys-subsystem-net-devices-{{ edge_can_interface }}.device
|
||||
# mosquitto.service = the local MQTT bridge broker e3oncan publishes to (loopback).
|
||||
After=sys-subsystem-net-devices-{{ edge_can_interface }}.device network-online.target mosquitto.service
|
||||
Wants=network-online.target mosquitto.service
|
||||
|
||||
[Service]
|
||||
User=open3e
|
||||
EnvironmentFile={{ edge_base_dir }}/open3e/open3e.env
|
||||
WorkingDirectory={{ edge_base_dir }}/e3oncan
|
||||
# Journald-friendly (python block-buffers pipes; lost on restarts).
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
ExecStart={{ edge_base_dir }}/e3oncan/venv/bin/python {{ edge_base_dir }}/e3oncan/src/E3onCANcollect.py \
|
||||
-c {{ edge_can_interface }} \
|
||||
-dev e380 \
|
||||
-g {{ edge_e3oncan_gap_seconds }} \
|
||||
-m {{ edge_mqtt_host }}:{{ edge_mqtt_port }}:{{ edge_open3e_mqtt_topic }} \
|
||||
-muser {{ edge_open3e_mqtt_user }} \
|
||||
-mpass ${OPEN3E_MQTT_PASSWORD} \
|
||||
-mfstr "{{ edge_e3oncan_mqtt_format }}" \
|
||||
-j
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,8 @@
|
||||
# Managed by ansible (roles/heating-can). 0600 -- carries the broker credential.
|
||||
#
|
||||
# Kept out of docker-compose.yml, which is 0644: the sibling roles on this host
|
||||
# do the same (power-meter-sml renders its config at 0600 while its compose file
|
||||
# stays world-readable), and the native unit this replaced used a 0600 env file
|
||||
# for exactly this value.
|
||||
MQTT_USER={{ edge_open3e_mqtt_user }}
|
||||
MQTT_PASSWORD={{ edge_open3e_mqtt_password }}
|
||||
@@ -1,7 +0,0 @@
|
||||
# Managed by ansible (roles/heating-can). Read by open3e.service and
|
||||
# e3oncan.service.
|
||||
# Full user:password pair as one variable so the open3e unit can pass it
|
||||
# as a single -muser argument without exposing it in the unit file;
|
||||
# e3oncan takes user and password separately (-muser/-mpass).
|
||||
OPEN3E_MUSER={{ edge_open3e_mqtt_user }}:{{ edge_open3e_mqtt_password }}
|
||||
OPEN3E_MQTT_PASSWORD={{ edge_open3e_mqtt_password }}
|
||||
@@ -1,39 +0,0 @@
|
||||
# Managed by ansible (roles/heating-can).
|
||||
# Read-mostly listener — no -w flag, no service 0x77. The ONE sanctioned
|
||||
# write path is the MQTT command topic (-l): HA publishes the DHW
|
||||
# one-time-charge command there (scope decision 2026-07-02, see vican
|
||||
# CLAUDE.md). Anything beyond that single write gets its own review.
|
||||
# -j is load-bearing: without it open3e flattens JSON payloads into
|
||||
# per-key subtopics and the HA package (homeassistant repo,
|
||||
# packages/open3e.yaml) receives nothing.
|
||||
# Exactly ONE open3e instance may be on the bus at a time (CAN collisions
|
||||
# stop open3e) — stop this service before running ad-hoc scans.
|
||||
[Unit]
|
||||
Description=open3e Viessmann E3 CAN-to-MQTT bridge (read-only)
|
||||
# Tie the service to the CAN device: stops on unplug, starts on plug via
|
||||
# the device unit rather than polling.
|
||||
BindsTo=sys-subsystem-net-devices-{{ edge_can_interface }}.device
|
||||
# mosquitto.service = the local MQTT bridge broker open3e publishes to (loopback).
|
||||
After=sys-subsystem-net-devices-{{ edge_can_interface }}.device network-online.target mosquitto.service
|
||||
Wants=network-online.target mosquitto.service
|
||||
|
||||
[Service]
|
||||
User=open3e
|
||||
EnvironmentFile={{ edge_base_dir }}/open3e/open3e.env
|
||||
WorkingDirectory={{ edge_base_dir }}/open3e
|
||||
ExecStart={{ edge_base_dir }}/open3e/venv/bin/open3e \
|
||||
-c {{ edge_can_interface }} \
|
||||
-cnfg {{ edge_base_dir }}/open3e/devices.json \
|
||||
-m {{ edge_mqtt_host }}:{{ edge_mqtt_port }}:{{ edge_open3e_mqtt_topic }} \
|
||||
-muser ${OPEN3E_MUSER} \
|
||||
-mcid open3e-edge \
|
||||
-mfstr "{{ edge_open3e_mqtt_format }}" \
|
||||
-r {{ edge_open3e_datapoints }} \
|
||||
-t {{ edge_open3e_poll_seconds }} \
|
||||
-l {{ edge_open3e_mqtt_topic }}/cmnd \
|
||||
-j
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -5,8 +5,6 @@
|
||||
# — off the untrusted IoT VLAN — and buffers telemetry in RAM during cluster
|
||||
# blips. The local broker writes nothing to the eMMC (persistence off, logs to
|
||||
# the volatile journal). See edge-mqtt-bridge.conf.j2 and README "MQTT bridge".
|
||||
edge_mqtt_host: 127.0.0.1
|
||||
edge_mqtt_port: 1883
|
||||
# Upstream cluster broker the local bridge connects to (TLS; the public
|
||||
# Let's Encrypt cert is validated against the system CA bundle).
|
||||
edge_mqtt_cluster_host: mqtt.renner.uno
|
||||
|
||||
@@ -78,6 +78,29 @@ For each, find the **current pin in the repo** (Read/Glob/Grep), then look up th
|
||||
6. **Ansible roles & OpenTofu providers** — `ansible/requirements.yml` (Galaxy
|
||||
roles/collections) and `opentofu/providers.tf` + `opentofu/.terraform.lock.hcl`.
|
||||
Report any with a newer published version.
|
||||
7. **Edge heating CAN services** — open3e (Vitocal heat pump, Vitocharge VX3)
|
||||
and E3onCAN (E380 grid energy meter), shipped as one image built from
|
||||
`ci/images/heating-can/Containerfile` and deployed by
|
||||
`ansible/roles/heating-can`. Nothing else tracks them. They read and (for two
|
||||
datapoints) write the heat pump over its CAN bus, so silent drift here
|
||||
matters more than most.
|
||||
|
||||
Both upstream pins are `ARG`s in the Containerfile -- `OPEN3E_REF` and
|
||||
`E3ONCAN_REF` -- not Ansible variables. Read them there, and note the
|
||||
awkward bits so normal states are not reported as problems:
|
||||
- open3e publishes **no tags at all** (its tags API returns an empty array),
|
||||
so compare `OPEN3E_REF` against the default branch:
|
||||
`https://api.github.com/repos/open3e/open3e/commits?per_page=1`. Report how
|
||||
many commits behind and the dates. "No release found" is normal here.
|
||||
- E3onCAN tags, but sparsely, and `E3ONCAN_REF` is an untagged commit later
|
||||
than the newest tag. Compare against both
|
||||
`https://api.github.com/repos/MyHomeMyData/E3onCAN/tags` and the default
|
||||
branch, and say which the pin sits between.
|
||||
|
||||
The deployed image tag is `heating_can_image` in
|
||||
`ansible/roles/heating-can/defaults/main.yml`. It is `:latest` today, so the
|
||||
node follows whatever CI last built -- the pins above are what actually fix
|
||||
the versions, and bumping them is what requires a rebuild.
|
||||
|
||||
## Method & rigor
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Heating system over the Viessmann E3 CAN bus: open3e (Vitocal heat pump and
|
||||
# Vitocharge VX3) and E3onCAN (E380 grid energy meter).
|
||||
#
|
||||
# One image, two services. They read the same physical installation over the
|
||||
# same bus and are versioned together, so they are one artifact -- but they run
|
||||
# as two containers from this image so either can crash and restart on its own
|
||||
# without a supervisor as PID 1.
|
||||
#
|
||||
# Why an image at all: both were pip-installed into venvs on the host, open3e
|
||||
# straight from a git URL. pip cannot compare an installed VCS dist against a
|
||||
# requested one without fetching it, so `pip install` re-cloned and rebuilt on
|
||||
# every Ansible converge -- reporting changed, restarting the service, and
|
||||
# putting a gap in heating telemetry each time. Pinning the SHA did not fix
|
||||
# that (verified: pip still clones and rebuilds a pinned VCS requirement). A
|
||||
# tagged image does: `docker compose up -d` on an unchanged tag is a genuine
|
||||
# no-op.
|
||||
#
|
||||
# It also gives the weekly update-audit a normal artifact to track instead of
|
||||
# two awkward special cases -- open3e publishes no tags at all, and E3onCAN is
|
||||
# pinned to an untagged commit between releases.
|
||||
#
|
||||
# Runtime needs host networking: SocketCAN interfaces live in the host network
|
||||
# namespace, so can0 is only reachable with network_mode: host. That is also
|
||||
# how the local MQTT broker on 127.0.0.1:1883 is reached. CAN_RAW sockets need
|
||||
# no capabilities, so both run unprivileged.
|
||||
FROM python:3.13-slim AS build
|
||||
|
||||
# Pins live here, in the artifact, rather than in Ansible variables.
|
||||
#
|
||||
# open3e: a commit, because the project publishes NO tags (its tags API returns
|
||||
# an empty array). Self-reported package version at this commit: 0.7.5.
|
||||
ARG OPEN3E_REF=6553c10453a6bae1210908f38a5af2481a46a3e5
|
||||
# E3onCAN: an untagged commit on the default branch (2026-06-21, "Fixed: MQTT
|
||||
# client ID collision caused stale retained values"). Later than the repo's
|
||||
# only tag, v0.5.1.
|
||||
ARG E3ONCAN_REF=461dcbdf97e8d72d2edb9929c63b39382af6c3a1
|
||||
|
||||
# git is needed to FETCH the sources and exists only in this stage. The runtime
|
||||
# stage below copies the built venv and the E3onCAN source out, so neither git
|
||||
# nor the apt cache ends up in the published image.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends git ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN python -m venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
RUN pip install --no-cache-dir supervisor \
|
||||
&& pip install --no-cache-dir "git+https://github.com/open3e/open3e.git@${OPEN3E_REF}"
|
||||
|
||||
# E3onCAN is not packaged -- it is run as a script out of its checkout, so the
|
||||
# source has to travel into the runtime image alongside its dependencies.
|
||||
RUN git clone --quiet https://github.com/MyHomeMyData/E3onCAN.git /opt/e3oncan \
|
||||
&& git -C /opt/e3oncan checkout --quiet "${E3ONCAN_REF}" \
|
||||
&& pip install --no-cache-dir -r /opt/e3oncan/requirements.txt \
|
||||
&& rm -rf /opt/e3oncan/.git
|
||||
|
||||
|
||||
FROM python:3.13-slim
|
||||
|
||||
ARG OPEN3E_REF=6553c10453a6bae1210908f38a5af2481a46a3e5
|
||||
ARG E3ONCAN_REF=461dcbdf97e8d72d2edb9929c63b39382af6c3a1
|
||||
|
||||
LABEL org.opencontainers.image.title="heating-can" \
|
||||
org.opencontainers.image.description="open3e + E3onCAN, Viessmann E3 CAN bus readers" \
|
||||
org.opencontainers.image.source="https://gitea.renner.uno/k.renner/homelab" \
|
||||
uno.renner.open3e.ref="${OPEN3E_REF}" \
|
||||
uno.renner.e3oncan.ref="${E3ONCAN_REF}"
|
||||
|
||||
# Everything is installed at build time. The runtime image carries the finished
|
||||
# venv and source only -- no git, no compiler, no package index.
|
||||
COPY --from=build /opt/venv /opt/venv
|
||||
COPY --from=build /opt/e3oncan /opt/e3oncan
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
# Both services log to stdout, which docker's journald driver forwards to the
|
||||
# journal and edge-log-shipper ships to Loki. open3e's systemd unit did NOT set
|
||||
# this, so its output was block-buffered and effectively never reached the
|
||||
# journal -- a long-standing blind spot that this fixes for both.
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# One container, two services, so PID 1 is a supervisor. It restarts either
|
||||
# reader on its own if it dies, reaps children and forwards shutdown -- which a
|
||||
# bare process as PID 1 cannot do for a sibling.
|
||||
#
|
||||
# Configuration arrives as environment variables and supervisord expands them,
|
||||
# so nothing outside this image constructs open3e's or E3onCAN's argv. Those
|
||||
# CLIs are an implementation detail here; a compose file spelling them out
|
||||
# would break whenever either upstream renamed a flag.
|
||||
COPY supervisord.conf /etc/supervisord.conf
|
||||
|
||||
# Build-time self-test. Fails the build if either service cannot actually run,
|
||||
# which is exactly what shipped before: supervisord pointed at
|
||||
# /opt/e3oncan/src/E3onCANcollect.py, a path that does not exist -- the file is
|
||||
# at the repo root. The image built green and would have run open3e while
|
||||
# e3oncan crash-looped, the E380 meter silently absent.
|
||||
#
|
||||
# Done here rather than as a CI step because this needs no registry
|
||||
# credentials and no runner able to pull a private image -- and because a
|
||||
# broken image should never be published in the first place.
|
||||
RUN open3e --help > /dev/null \
|
||||
&& python /opt/e3oncan/E3onCANcollect.py --help > /dev/null \
|
||||
&& supervisord --help > /dev/null \
|
||||
&& grep -q '^\[program:open3e\]' /etc/supervisord.conf \
|
||||
&& grep -q '^\[program:e3oncan\]' /etc/supervisord.conf \
|
||||
&& echo "self-test OK: both services execute; supervisord has both programs"
|
||||
|
||||
# CAN_RAW needs no capabilities, so nothing here runs as root.
|
||||
RUN useradd --system --no-create-home --shell /usr/sbin/nologin open3e
|
||||
|
||||
# A writable working directory, owned by the service user. udsoncan writes its
|
||||
# log file into the CWD, and the native install this replaces gave open3e's
|
||||
# directory `owner: open3e` for exactly that reason. /opt is root-owned from the
|
||||
# COPY layers, so running there would fail that write.
|
||||
RUN install -d -o open3e -g open3e -m 0755 /var/lib/open3e
|
||||
|
||||
USER open3e
|
||||
WORKDIR /var/lib/open3e
|
||||
|
||||
ENTRYPOINT ["supervisord", "-c", "/etc/supervisord.conf"]
|
||||
@@ -0,0 +1,62 @@
|
||||
# Both CAN readers, supervised in one container.
|
||||
#
|
||||
# They read the same heating installation over the same bus and are versioned
|
||||
# and deployed together, so they are one container. A container runs one PID 1;
|
||||
# with two processes that PID 1 is a supervisor, which is what this is --
|
||||
# supervisord restarts either service on its own if it dies, reaps children,
|
||||
# and forwards shutdown.
|
||||
#
|
||||
# Configuration comes from the environment (see the compose file). supervisord
|
||||
# expands %(ENV_NAME)s at start, so neither the compose file nor a wrapper
|
||||
# script constructs open3e's or E3onCAN's argv -- those CLIs stay an
|
||||
# implementation detail of this image.
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
user=open3e
|
||||
logfile=/dev/null
|
||||
logfile_maxbytes=0
|
||||
pidfile=/tmp/supervisord.pid
|
||||
|
||||
# open3e: UDS request/response to the Vitocal heat pump and Vitocharge VX3.
|
||||
[program:open3e]
|
||||
command=open3e
|
||||
-c %(ENV_CAN_INTERFACE)s
|
||||
-cnfg %(ENV_OPEN3E_CONFIG)s
|
||||
-m %(ENV_MQTT_HOST)s:%(ENV_MQTT_PORT)s:%(ENV_MQTT_TOPIC)s
|
||||
-muser %(ENV_MQTT_USER)s:%(ENV_MQTT_PASSWORD)s
|
||||
-mcid %(ENV_OPEN3E_CLIENT_ID)s
|
||||
-mfstr %(ENV_MQTT_FORMAT)s
|
||||
-r %(ENV_OPEN3E_DATAPOINTS)s
|
||||
-t %(ENV_OPEN3E_POLL_SECONDS)s
|
||||
-l %(ENV_MQTT_TOPIC)s/cmnd
|
||||
-j
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
stopsignal=INT
|
||||
# Straight to the container's stdout/stderr, so docker's journald driver picks
|
||||
# them up and edge-log-shipper ships them to Loki.
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
|
||||
# e3oncan: passive CANopen listener for the E380 grid energy meter, which feeds
|
||||
# the heat pump's own energy management. Issues no bus writes, which is why the
|
||||
# two can share the bus.
|
||||
[program:e3oncan]
|
||||
command=python /opt/e3oncan/E3onCANcollect.py
|
||||
-c %(ENV_CAN_INTERFACE)s
|
||||
-dev %(ENV_E3ONCAN_DEVICE)s
|
||||
-g %(ENV_E3ONCAN_GAP_SECONDS)s
|
||||
-m %(ENV_MQTT_HOST)s:%(ENV_MQTT_PORT)s:%(ENV_MQTT_TOPIC)s
|
||||
-muser %(ENV_MQTT_USER)s
|
||||
-mpass %(ENV_MQTT_PASSWORD)s
|
||||
-mfstr %(ENV_E3ONCAN_MQTT_FORMAT)s
|
||||
-j
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
stopsignal=INT
|
||||
stdout_logfile=/dev/fd/1
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
@@ -38,7 +38,7 @@ spec:
|
||||
# gets the heavy HA pod (8Gi) off the soon-to-be-tiny edge box.
|
||||
containers:
|
||||
- name: home-assistant
|
||||
image: ghcr.io/home-assistant/home-assistant:2026.8.0 # {"$imagepolicy": "flux-system:home-assistant"}
|
||||
image: ghcr.io/home-assistant/home-assistant:2026.8.1 # {"$imagepolicy": "flux-system:home-assistant"}
|
||||
env:
|
||||
- name: TZ
|
||||
value: Europe/Berlin
|
||||
|
||||
@@ -27,7 +27,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: pve-exporter
|
||||
image: prompve/prometheus-pve-exporter:3.9.0 # {"$imagepolicy": "flux-system:pve-exporter"}
|
||||
image: prompve/prometheus-pve-exporter:3.10.0 # {"$imagepolicy": "flux-system:pve-exporter"}
|
||||
# Image ENTRYPOINT is already `pve_exporter`; pass flags only.
|
||||
args:
|
||||
- --config.file=/etc/pve-exporter/pve.yml
|
||||
|
||||
@@ -22,7 +22,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: gotenberg
|
||||
image: gotenberg/gotenberg:8.34.0 # {"$imagepolicy": "flux-system:gotenberg"}
|
||||
image: gotenberg/gotenberg:8.35.0 # {"$imagepolicy": "flux-system:gotenberg"}
|
||||
args:
|
||||
- "gotenberg"
|
||||
- "--chromium-disable-javascript=true"
|
||||
|
||||
Reference in New Issue
Block a user