edge-host was a single role doing eight unrelated jobs: flash-wear
mitigation, log shipping, VLAN networking, an MQTT bridge, a CAN interface,
the heating CAN services, a Z-Wave gateway and a power-meter reader.
That is not just untidy. It is what blocked a second thin client from
joining as an NTP peer: the host-generic work (journald to RAM, Alloy, the
VLAN sub-interface) could not be applied without also handing the box a
Z-Wave radio and a CAN bus it does not have. The role bound its services to
specific USB device paths, so a second host would have tried to start
containers for hardware that is not there.
Now:
edge-flash-wear journald -> RAM, noatime/lazytime, TRIM, eMMC health
edge-log-shipper Alloy: journal -> Loki
edge-vlan tagged VLAN sub-interface
mqtt-bridge loopback mosquitto -> cluster broker over TLS
socketcan can0 only
heating-can open3e (Vitocal/VX3) + e3oncan (E380 meter)
zwave-gateway zwave-js-ui, own compose project
power-meter-sml sml2mqtt, own compose project
The E380 reader lives with open3e rather than in its own role: it is part
of the same heating installation -- it feeds the heat pump's own energy
management -- not a separate system that happens to share a bus. can0 does
stay separate, because an interface is not the same concern as the services
consuming it.
Each role installs its own packages, so it stands alone. An edge host with
no peripherals takes the first five and none of the rest.
docker-daemon.json moves to docker-install with its own docker_log_driver
variable and Restart docker handler, since configuring the docker daemon was
never an edge-host concern. Handlers split ten ways; defaults split per
role; all sixteen templates and files moved with git mv.
Two deliberate behaviour changes on the node, both accepted:
1. Compose split. zwave-js and sml2mqtt shared a compose file, so touching
either recreated both. They are now separate projects in their own
directories. container_name is fixed, so the old project must release the
names first -- a guarded one-time task downs it and removes the file.
First apply costs a brief down/up on both. Data paths are unchanged; the
zwave store is not migrated.
2. Header repointing. Templates and scripts carried "Managed by
roles/edge-host" headers, which render onto the host -- they would have
sent an operator to a role that no longer exists. Updating them changes
file content, so it restarts the services that consume those files.
Verified by --check --diff against the live host. 24 changed items, and
every one is accounted for:
5 pre-existing, unchanged from the baseline before this work:
common timesyncd; open3e venv; E3onCAN venv; and the open3e /
e3oncan restart handlers. The venv tasks install from a git URL,
which pip cannot verify, so they report changed on every run --
a separate defect, not introduced here.
5 the compose split: zwave env + compose and sml compose to their
new paths, plus the two legacy-project retirement tasks.
14 the header repointing: journald conf, eMMC health script, alloy
config, mosquitto bridge config, 80-can0.network, open3e overlay /
env / unit, e3oncan unit, zwave backup script + env, sml2mqtt
config -- and the resulting restarts of journald, alloy, mosquitto,
open3e and e3oncan.
Nothing changed outside those three groups.
One consequence check mode cannot show: 80-can0.network.j2 changed, so the
"Reload networkd" handler fires on a real run and reconfigures can0,
briefly dropping the bus under open3e and e3oncan. It is absent from the
check-mode output only because shell handlers are skipped there. The VLAN
templates did NOT change, so the riskier reconfigure of the parent uplink
on this headless host is not triggered.
Also fixed in passing: the keepalived-ntp guard told operators to recover
with `--limit <host> --tags vlan40`, and referred to the edge-host role.
Both corrected. The role README becomes ansible/docs/edge-host.md -- it
documents the host, not any single role now.
Co-Authored-By: Claude Bot <claude-bot@renner.uno>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
117 lines
3.4 KiB
YAML
117 lines
3.4 KiB
YAML
---
|
|
# Heating system over the Viessmann E3 CAN bus.
|
|
#
|
|
# Two readers of the same heating installation:
|
|
# 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
|
|
#
|
|
# 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.
|
|
#
|
|
# 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.
|
|
|
|
- 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
|
|
ansible.builtin.file:
|
|
path: "{{ edge_base_dir }}/open3e"
|
|
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
|
|
|
|
- name: Deploy open3e vx3 datapoint overlay (battery-module DIDs)
|
|
ansible.builtin.copy:
|
|
src: open3e-vx3plus.py
|
|
dest: "{{ edge_base_dir }}/open3e/vx3plus.py"
|
|
mode: "0644"
|
|
notify: Restart open3e
|
|
|
|
- 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)
|
|
ansible.builtin.template:
|
|
src: open3e.env.j2
|
|
dest: "{{ edge_base_dir }}/open3e/open3e.env"
|
|
owner: open3e
|
|
mode: "0600"
|
|
notify: Restart open3e
|
|
|
|
- name: Deploy open3e systemd unit
|
|
ansible.builtin.template:
|
|
src: open3e.service.j2
|
|
dest: /etc/systemd/system/open3e.service
|
|
mode: "0644"
|
|
notify: Restart open3e
|
|
|
|
- name: Enable open3e
|
|
ansible.builtin.systemd:
|
|
name: open3e
|
|
enabled: true
|
|
state: started
|
|
daemon_reload: true
|
|
|
|
|
|
# --- 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
|