From 924b7b5b348d41d2755504b3c151c46b6e2b839c Mon Sep 17 00:00:00 2001 From: Tomas Uvira Date: Fri, 1 May 2026 13:30:35 +0200 Subject: [PATCH] feat: initial scaffold and profiles for Schneider iEM2135, LUG heat meter v4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seed the repo described in pda-fieldbus ADR-0009: a sibling repo that ships device profiles independently of the fieldbus binary. Layout: - profiles/schneider-iem2135.json — distilled from the inline extract rules in examples/poll-d27-g110.yaml - profiles/lug-heat-meter-v4.json — heat-meter profile with derived delta_temperature Both validate against pda-fieldbus's profile.LoadDirs. Packaging: - nfpm.yaml builds pda-fieldbus-profiles.deb installing profiles/ to /usr/share/pda-fieldbus/profiles/, where the loader's DirPackaged dir picks them up. Recommends pda-fieldbus. - .gitea/workflows/auto-tag.yml: same conventional-commit auto-tagging as pda-fieldbus, on tag push installs nfpm, builds .deb, uploads to repo.pda.cz/PDAT/main using the existing PDA_REPO_TOKEN secret. - .gitea/workflows/ci.yml: JSON syntax check + schema validation by importing pda-fieldbus's loader and calling LoadDirs against profiles/. --- .gitea/workflows/auto-tag.yml | 96 +++++++++++++++++++++++++++++++++ .gitea/workflows/ci.yml | 60 +++++++++++++++++++++ .gitignore | 4 ++ LICENSE | 21 ++++++++ Makefile | 12 +++++ README.md | 61 +++++++++++++++++++++ nfpm.yaml | 28 ++++++++++ profiles/lug-heat-meter-v4.json | 54 +++++++++++++++++++ profiles/schneider-iem2135.json | 35 ++++++++++++ 9 files changed, 371 insertions(+) create mode 100644 .gitea/workflows/auto-tag.yml create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 nfpm.yaml create mode 100644 profiles/lug-heat-meter-v4.json create mode 100644 profiles/schneider-iem2135.json diff --git a/.gitea/workflows/auto-tag.yml b/.gitea/workflows/auto-tag.yml new file mode 100644 index 0000000..1881e42 --- /dev/null +++ b/.gitea/workflows/auto-tag.yml @@ -0,0 +1,96 @@ +name: Auto Tag + +on: + push: + branches: [main] + +permissions: + contents: write + +jobs: + tag: + runs-on: ubuntu-latest + outputs: + new_tag: ${{ steps.bump.outputs.new_tag }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Determine version bump and create tag + id: bump + run: | + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") + echo "Latest tag: $LATEST_TAG" + + COMMITS=$(git log "${LATEST_TAG}..HEAD" --pretty=format:"%s" 2>/dev/null) + if [ -z "$COMMITS" ]; then + echo "No new commits since ${LATEST_TAG}, skipping." + exit 0 + fi + + BUMP="" + if echo "$COMMITS" | grep -qiE "^[a-z]+(\(.+\))?!:|BREAKING CHANGE"; then + BUMP="major" + elif echo "$COMMITS" | grep -qiE "^feat(\(.+\))?:"; then + BUMP="minor" + elif echo "$COMMITS" | grep -qiE "^fix(\(.+\))?:"; then + BUMP="patch" + fi + + if [ -z "$BUMP" ]; then + echo "No conventional commit triggers, skipping." + exit 0 + fi + + VERSION="${LATEST_TAG#v}" + MAJOR=$(echo "$VERSION" | cut -d. -f1) + MINOR=$(echo "$VERSION" | cut -d. -f2) + PATCH=$(echo "$VERSION" | cut -d. -f3) + + case "$BUMP" in + major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;; + minor) MINOR=$((MINOR + 1)); PATCH=0 ;; + patch) PATCH=$((PATCH + 1)) ;; + esac + + NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}" + echo "Creating tag: $NEW_TAG" + + git tag "$NEW_TAG" + git push origin "$NEW_TAG" + echo "new_tag=$NEW_TAG" >> "$GITHUB_OUTPUT" + + release: + needs: tag + if: needs.tag.outputs.new_tag != '' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.tag.outputs.new_tag }} + + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache: false + + - name: Install nfpm + run: go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.41.3 + + - name: Build .deb + run: | + VER="${{ needs.tag.outputs.new_tag }}" + VER="${VER#v}" + mkdir -p dist + VERSION="$VER" nfpm pkg --packager deb --target dist/ -f nfpm.yaml + ls -la dist/ + + - name: Publish .deb to repo.pda.cz + run: | + for deb in dist/*.deb; do + echo "Uploading $deb" + curl --fail -X POST https://repo.pda.cz/api/v1/PDAT/main/upload \ + -H "Authorization: Bearer ${{ secrets.PDA_REPO_TOKEN }}" \ + -F "file=@${deb}" + done diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..29b1a58 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,60 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache: false + + - name: JSON syntax check + run: | + for f in profiles/*.json; do + echo "checking $f" + python3 -m json.tool "$f" > /dev/null + done + + - name: Schema validation via pda-fieldbus loader + working-directory: ${{ runner.temp }} + run: | + mkdir -p validator && cd validator + go mod init validator + go get github.com/pdat-cz/pda-fieldbus@latest + cat > main.go <<'EOF' + package main + + import ( + "fmt" + "os" + + "github.com/pdat-cz/pda-fieldbus/pkg/proto/profile" + ) + + func main() { + if len(os.Args) != 2 { + fmt.Fprintln(os.Stderr, "usage: validator ") + os.Exit(2) + } + m, err := profile.LoadDirs(os.Args[1]) + if err != nil { + fmt.Fprintln(os.Stderr, "FAIL:", err) + os.Exit(1) + } + for n, p := range m { + fmt.Printf("OK %s device=%s/%s points=%d derived=%d\n", + n, p.Device.Manufacturer, p.Device.Model, + len(p.Points), len(p.Derived)) + } + } + EOF + go run . "$GITHUB_WORKSPACE/profiles" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20abc13 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/dist/ +/.cache/ +*.deb +*.tar.gz diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2ca4d0c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 PDA Servers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2c218af --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +VERSION ?= 0.0.0-dev + +.PHONY: package clean + +# Build a .deb at $(VERSION). Requires nfpm in PATH. +# Example: make package VERSION=0.1.0 +package: + mkdir -p dist + VERSION=$(VERSION) nfpm pkg --packager deb --target dist/ -f nfpm.yaml + +clean: + rm -rf dist/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..8516337 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# pda-fieldbus-profiles + +Reusable device profiles for [pda-fieldbus](https://git.pda.cz/PDAT/pda-fieldbus). + +A device profile is a JSON description of one device model: which +DIF/VIF records to read on M-Bus (or which registers on Modbus), what +units they have, how to scale them, and any derived values to compute +from them. Profiles let `pda-fieldbus poll` configurations stay short +(`profile: schneider-iem2135`) instead of repeating extract rules per +deployment. + +See `pda-fieldbus` ADR-0009 and spec PDA-0010 for the full schema and +loader semantics. + +## Layout + +``` +profiles/ + schneider-iem2135.json + lug-heat-meter-v4.json + ... +``` + +Each file is one profile. The filename stem is the profile name +referenced from a poll config (`profile: schneider-iem2135`). + +## Installation + +The `pda-fieldbus-profiles` `.deb` installs files to: + +``` +/usr/share/pda-fieldbus/profiles/ +``` + +The `pda-fieldbus` loader scans that directory plus +`/etc/pda-fieldbus/profiles.d/` (operator overrides — wins on filename +collision). To override a packaged profile on one box, copy it to the +operator dir and edit there; the package will not overwrite it. + +## Adding a profile + +1. Create `profiles/.json` following an existing profile. +2. Validate it loads against the current `pda-fieldbus` loader: + ``` + pda-fieldbus poll --config --profile-dir ./profiles --dry-run + ``` +3. Open a PR. Use a `feat:` commit (`feat: add - profile`). + The `feat:` prefix triggers a minor version bump and a release of the + `.deb` to repo.pda.cz. + +## Conventional commits + +Same convention as `pda-fieldbus`: + +- `feat:` — new profile (minor bump) +- `fix:` — correction to an existing profile (patch bump) +- `docs:`, `chore:`, `ci:` — no version bump + +## License + +MIT. See `LICENSE`. diff --git a/nfpm.yaml b/nfpm.yaml new file mode 100644 index 0000000..76bc37f --- /dev/null +++ b/nfpm.yaml @@ -0,0 +1,28 @@ +# nfpm config — package profiles/ as /usr/share/pda-fieldbus/profiles/ +# Version is injected at build time: +# nfpm pkg --packager deb --target dist/ -f nfpm.yaml -v "$VERSION" + +name: pda-fieldbus-profiles +arch: all +platform: linux +version: ${VERSION} +section: contrib/utils +priority: optional +maintainer: p.d.a. +vendor: p.d.a. +homepage: https://git.pda.cz/PDAT/pda-fieldbus-profiles +license: MIT +description: | + Device profiles for pda-fieldbus. + Reusable JSON descriptions of M-Bus and Modbus device models — points, + units, scaling, and derived values — consumed by pda-fieldbus poll + via `profile: ` references. + +contents: + - src: profiles/ + dst: /usr/share/pda-fieldbus/profiles/ + type: tree + +deb: + fields: + Recommends: pda-fieldbus diff --git a/profiles/lug-heat-meter-v4.json b/profiles/lug-heat-meter-v4.json new file mode 100644 index 0000000..dde17fd --- /dev/null +++ b/profiles/lug-heat-meter-v4.json @@ -0,0 +1,54 @@ +{ + "schema": "pda-fieldbus.profile/v1", + "device": { + "manufacturer": "LUG", + "model": "heat-meter-v4", + "protocol": "mbus" + }, + "meta": { + "version": "1" + }, + "points": { + "energy": { + "addr": "mbus/dif:0C/vif:0F", + "unit": "gigajoule", + "scale": 1e-9, + "dimensions": "energy", + "description": "Total heat energy" + }, + "power": { + "addr": "mbus/dif:0B/vif:2D", + "unit": "kilowatt", + "scale": 0.001, + "dimensions": "power", + "description": "Instantaneous thermal power" + }, + "flow_temperature": { + "addr": "mbus/dif:0A/vif:5B", + "unit": "celsius", + "dimensions": "temperature", + "description": "Supply (flow) temperature" + }, + "return_temperature": { + "addr": "mbus/dif:0A/vif:5F", + "unit": "celsius", + "dimensions": "temperature", + "description": "Return temperature" + }, + "volume_flow": { + "addr": "mbus/dif:0B/vif:3B", + "unit": "cubic_metre_per_hour", + "dimensions": "flow", + "description": "Volumetric flow rate" + } + }, + "derived": { + "delta_temperature": { + "function": "sub", + "inputs": ["flow_temperature", "return_temperature"], + "unit": "celsius", + "dimensions": "temperature", + "description": "Flow minus return temperature" + } + } +} diff --git a/profiles/schneider-iem2135.json b/profiles/schneider-iem2135.json new file mode 100644 index 0000000..ef899a1 --- /dev/null +++ b/profiles/schneider-iem2135.json @@ -0,0 +1,35 @@ +{ + "schema": "pda-fieldbus.profile/v1", + "device": { + "manufacturer": "Schneider Electric", + "model": "iEM2135", + "protocol": "mbus" + }, + "meta": { + "version": "1", + "references": [ + "Schneider iEM2000 series user manual" + ] + }, + "points": { + "current": { + "addr": "mbus/dif:05/vif:FD/vife:DC,FF,00", + "unit": "ampere", + "dimensions": "current", + "description": "Total current" + }, + "power": { + "addr": "mbus/dif:05/vif:2E", + "unit": "watt", + "dimensions": "power", + "description": "Active power" + }, + "energy": { + "addr": "mbus/dif:07/vif:03", + "unit": "kilowatt_hour", + "scale": 0.001, + "dimensions": "energy", + "description": "Total active energy import" + } + } +} \ No newline at end of file